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 *     nulrich
016 *
017 * $Id
018 */
019package org.nuxeo.ecm.platform.forum.web;
020
021import static org.jboss.seam.ScopeType.EVENT;
022
023import java.security.Principal;
024import java.util.ArrayList;
025import java.util.Arrays;
026import java.util.Collections;
027import java.util.GregorianCalendar;
028import java.util.List;
029
030import javax.faces.context.FacesContext;
031
032import org.apache.commons.logging.Log;
033import org.apache.commons.logging.LogFactory;
034import org.jboss.seam.ScopeType;
035import org.jboss.seam.annotations.Factory;
036import org.jboss.seam.annotations.In;
037import org.jboss.seam.annotations.Name;
038import org.jboss.seam.annotations.Scope;
039import org.jboss.seam.core.Events;
040import org.nuxeo.ecm.core.api.CoreSession;
041import org.nuxeo.ecm.core.api.DocumentModel;
042import org.nuxeo.ecm.core.api.DocumentNotFoundException;
043import org.nuxeo.ecm.core.api.DocumentRef;
044import org.nuxeo.ecm.core.api.NuxeoGroup;
045import org.nuxeo.ecm.core.api.NuxeoPrincipal;
046import org.nuxeo.ecm.core.api.pathsegment.PathSegmentService;
047import org.nuxeo.ecm.core.api.security.SecurityConstants;
048import org.nuxeo.ecm.platform.comment.web.CommentManagerActions;
049import org.nuxeo.ecm.platform.comment.web.ThreadEntry;
050import org.nuxeo.ecm.platform.forum.web.api.PostAction;
051import org.nuxeo.ecm.platform.forum.web.api.ThreadAction;
052import org.nuxeo.ecm.platform.forum.web.api.ThreadAdapter;
053import org.nuxeo.ecm.platform.forum.workflow.ForumConstants;
054import org.nuxeo.ecm.platform.ui.web.api.NavigationContext;
055import org.nuxeo.ecm.webapp.helpers.EventNames;
056import org.nuxeo.ecm.webapp.helpers.ResourcesAccessor;
057import org.nuxeo.runtime.api.Framework;
058
059/**
060 * This Action Listener represents a Thread inside a forum.
061 *
062 * @author <a href="bchaffangeon@nuxeo.com">Brice Chaffangeon</a>
063 */
064@Name("threadAction")
065@Scope(ScopeType.CONVERSATION)
066public class ThreadActionBean implements ThreadAction {
067
068    private static final Log log = LogFactory.getLog(ThreadActionBean.class);
069
070    private static final long serialVersionUID = -2667460487440135732L;
071
072    protected static final String schema = "thread";
073
074    protected static final String type = "Thread";
075
076    protected boolean principalIsAdmin;
077
078    @In(create = true)
079    protected transient Principal currentUser;
080
081    @In(create = true, required = false)
082    protected transient CoreSession documentManager;
083
084    @In(create = true)
085    protected transient CommentManagerActions commentManagerActions;
086
087    @In(create = true)
088    protected NavigationContext navigationContext;
089
090    @In(create = true)
091    protected ResourcesAccessor resourcesAccessor;
092
093    @In(create = true)
094    protected PostAction postAction;
095
096    protected String title;
097
098    protected String description;
099
100    protected List<String> selectedModerators;
101
102    protected boolean moderated;
103
104    protected NuxeoPrincipal principal;
105
106    public String addThread() {
107
108        // The thread to be created
109        DocumentModel docThread = getThreadModel();
110
111        docThread = documentManager.createDocument(docThread);
112        documentManager.save();
113
114        DocumentModel currentDocument = navigationContext.getCurrentDocument();
115        Events.instance().raiseEvent(EventNames.DOCUMENT_CHILDREN_CHANGED, currentDocument);
116        clean();
117        return navigationContext.navigateToDocument(docThread, "after-create");
118    }
119
120    /**
121     * Clean variables.
122     */
123    protected void clean() {
124        title = null;
125        description = null;
126        moderated = false;
127        selectedModerators = null;
128    }
129
130    /**
131     * Gets the Thread to create as a DocumentModel.
132     */
133    protected DocumentModel getThreadModel() {
134
135        DocumentModel currentDocument = navigationContext.getCurrentDocument();
136        String path = currentDocument.getPathAsString();
137
138        final DocumentModel docThread = documentManager.createDocumentModel(type);
139        docThread.setProperty("dublincore", "title", title);
140        docThread.setProperty("dublincore", "description", description);
141        docThread.setProperty(schema, "moderated", moderated);
142
143        if (moderated) {
144
145            List<String> selectedModerators = getSelectedModerators();
146
147            // The current user should have the right to moderate
148            if (!selectedModerators.contains(NuxeoPrincipal.PREFIX + currentUser.getName())) {
149                selectedModerators.add(NuxeoPrincipal.PREFIX + currentUser.getName());
150            }
151
152            // XXX: hack, administrators should have the right to moderate
153            // without being in this list
154            // We automatically add administrators (with prefix) as moderators
155            if (!selectedModerators.contains(NuxeoGroup.PREFIX + SecurityConstants.ADMINISTRATORS)) {
156                selectedModerators.add(NuxeoGroup.PREFIX + SecurityConstants.ADMINISTRATORS);
157            }
158            // We can also remove Administrator since his group is added
159            if (selectedModerators.contains(NuxeoPrincipal.PREFIX + SecurityConstants.ADMINISTRATOR)) {
160                selectedModerators.remove(NuxeoPrincipal.PREFIX + SecurityConstants.ADMINISTRATOR);
161            }
162
163            docThread.setProperty(schema, "moderators", selectedModerators);
164        }
165
166        PathSegmentService pss = Framework.getService(PathSegmentService.class);
167        docThread.setPathInfo(path, pss.generatePathSegment(docThread));
168        return docThread;
169    }
170
171    @SuppressWarnings({ "unchecked" })
172    public List<String> getModerators() {
173        DocumentModel currentThread = navigationContext.getCurrentDocument();
174        return (List<String>) currentThread.getProperty("thread", "moderators");
175    }
176
177    public boolean isPrincipalModerator() {
178        principal = (NuxeoPrincipal) FacesContext.getCurrentInstance().getExternalContext().getUserPrincipal();
179        List<String> moderators = getModerators();
180
181        boolean moderator = false;
182        if (isPrincipalGroupModerator() || moderators != null
183                && moderators.contains(NuxeoPrincipal.PREFIX + principal.getName())) {
184            moderator = true;
185        }
186        return moderator;
187    }
188
189    public boolean isPrincipalGroupModerator() {
190
191        List<String> moderators = getModerators();
192        List<String> principalGroups = principal.getAllGroups();
193
194        for (String principalGroup : principalGroups) {
195            if (moderators != null && moderators.contains(NuxeoGroup.PREFIX + principalGroup)) {
196                return true;
197            }
198        }
199        return false;
200    }
201
202    public boolean isCurrentThreadModerated() {
203        DocumentModel currentThread = navigationContext.getCurrentDocument();
204        return isThreadModerated(currentThread);
205    }
206
207    @Factory(value = "currentThreadPosts", scope = EVENT)
208    public List<ThreadEntry> getPostsAsThread() {
209        List<ThreadEntry> basicCommentList = null;
210
211        // Thread is not moderated, we return all Posts
212        if (!isCurrentThreadModerated()) {
213            basicCommentList = commentManagerActions.getCommentsAsThread();
214        } else {
215            // Here we clean the list according the rights of principal.
216            basicCommentList = new ArrayList<ThreadEntry>();
217            List<ThreadEntry> allThreadEntry = commentManagerActions.getCommentsAsThread();
218
219            for (ThreadEntry threadEntry : allThreadEntry) {
220                // if current user is not moderator and post is not published,
221                // we remove it
222                DocumentModel dm = threadEntry.getComment();
223
224                String[] contributorsArray = (String[]) dm.getProperty("dublincore", "contributors");
225                if (contributorsArray == null) {
226                    contributorsArray = new String[0];
227                }
228                List<String> cs = Arrays.asList(contributorsArray);
229
230                if (postAction.isPostPublished(threadEntry.getComment()) || isPrincipalModerator()
231                        || cs.contains(currentUser.getName())) {
232                    basicCommentList.add(threadEntry);
233                }
234            }
235        }
236        return basicCommentList;
237    }
238
239    protected ThreadAdapter adapter;
240
241    public ThreadAdapter getAdapter(DocumentModel thread) {
242        if (thread == null) {
243            return null;
244        }
245        if (adapter != null && adapter.getThreadDoc().getRef().equals(thread.getRef())) {
246            return adapter;
247        }
248        if (thread.getSessionId() == null) {
249            try {
250                thread = documentManager.getDocument(thread.getRef());
251            } catch (DocumentNotFoundException e) {
252                log.error("Unable to reconnect doc !,", e);
253            }
254        }
255        adapter = thread.getAdapter(ThreadAdapter.class);
256        return adapter;
257    }
258
259    public List<DocumentModel> getAllPosts(DocumentModel thread, String state) {
260
261        thread = getDocumentThreadModel(thread.getRef());
262        List<DocumentModel> allPosts = Collections.emptyList();
263        List<ThreadEntry> allThreadEntry = Collections.emptyList();
264
265        if (thread != null) {
266            allThreadEntry = commentManagerActions.getCommentsAsThreadOnDoc(thread);
267        }
268        if (allThreadEntry != null && !allThreadEntry.isEmpty()) {
269            allPosts = new ArrayList<DocumentModel>();
270            for (ThreadEntry entry : allThreadEntry) {
271                if (!"".equals(state) && state.equals(entry.getComment().getCurrentLifeCycleState())) {
272                    allPosts.add(entry.getComment());
273                } else if ("".equals(state)) {
274                    allPosts.add(entry.getComment());
275                }
276            }
277
278        }
279
280        return allPosts;
281    }
282
283    public List<DocumentModel> getPostsPublished(DocumentModel thread) {
284        return getAllPosts(thread, ForumConstants.PUBLISHED_STATE);
285    }
286
287    public List<DocumentModel> getPostsPending(DocumentModel thread) {
288        return getAllPosts(thread, ForumConstants.PENDING_STATE);
289    }
290
291    public String getDescription() {
292        return description;
293    }
294
295    public String getTitle() {
296        return title;
297    }
298
299    public void setDescription(String description) {
300        this.description = description;
301    }
302
303    public void setTitle(String title) {
304        this.title = title;
305    }
306
307    public String getSchema() {
308        return schema;
309    }
310
311    public String getType() {
312        return type;
313    }
314
315    public void saveState() {
316        log.info("PrePassivate");
317    }
318
319    public void readState() {
320        log.info("PostActivate");
321    }
322
323    public boolean isModerated() {
324        return moderated;
325    }
326
327    public void setModerated(boolean moderated) {
328        this.moderated = moderated;
329    }
330
331    public DocumentModel getLastPostPublished(DocumentModel thread) {
332
333        thread = getDocumentThreadModel(thread.getRef());
334        List<DocumentModel> posts = getPostsPublished(thread);
335        DocumentModel lastPost = null;
336        if (!posts.isEmpty()) {
337            lastPost = posts.get(0);
338            for (DocumentModel post : posts) {
339                GregorianCalendar lastPostDate = (GregorianCalendar) lastPost.getProperty("post", "creationDate");
340                GregorianCalendar postDate = (GregorianCalendar) post.getProperty("post", "creationDate");
341                if (postDate != null && postDate.after(lastPostDate)) {
342                    lastPost = post;
343                }
344            }
345
346        }
347        return lastPost;
348    }
349
350    public String getModerationAsString(DocumentModel thread) {
351        if (isThreadModerated(thread)) {
352            return resourcesAccessor.getMessages().get("label.forum.thread.moderated.yes");
353        }
354        return resourcesAccessor.getMessages().get("label.forum.thread.moderated.no");
355    }
356
357    public boolean isThreadModerated(DocumentModel thread) {
358        if (thread != null) {
359            thread = getDocumentThreadModel(thread.getRef());
360            if (thread != null) {
361                Boolean moderation = (Boolean) thread.getProperty("thread", "moderated");
362                if (moderation != null) {
363                    return moderation;
364                }
365            }
366        }
367        return false;
368    }
369
370    public DocumentModel getParentPost(int post) {
371        DocumentModel parentPost = null;
372
373        List<ThreadEntry> posts = getPostsAsThread();
374        if (post > 0 && post <= posts.size()) {
375            ThreadEntry parent = posts.get(post - 1);
376            ThreadEntry currentPost = posts.get(post);
377            if (currentPost.getDepth() == parent.getDepth() + 1) {
378                parentPost = parent.getComment();
379            }
380        }
381        return parentPost;
382    }
383
384    public boolean isParentPostPublished(int post) {
385
386        DocumentModel parent = getParentPost(post);
387        if (parent == null) {
388            return true;
389        } else if (ForumConstants.PUBLISHED_STATE.equals(parent.getCurrentLifeCycleState())) {
390            return true;
391        }
392        return false;
393    }
394
395    /**
396     * Gets the thread for a given document reference.
397     */
398    protected DocumentModel getDocumentThreadModel(DocumentRef threadRef) {
399        DocumentModel thread = null;
400        if (threadRef != null) {
401            thread = documentManager.getDocument(threadRef);
402        }
403        return thread;
404    }
405
406    public List<String> getSelectedModerators() {
407        if (selectedModerators == null) {
408            selectedModerators = new ArrayList<String>();
409        }
410        return selectedModerators;
411    }
412
413    public void setSelectedModerators(List<String> selectedModerators) {
414        this.selectedModerators = selectedModerators;
415    }
416
417}