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