001/*
002 *  Copyright 2015 Anyware Services
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 */
016package org.ametys.plugins.explorer.threads.actions;
017
018import java.io.ByteArrayInputStream;
019import java.io.IOException;
020import java.util.Date;
021import java.util.HashMap;
022import java.util.LinkedList;
023import java.util.List;
024import java.util.Map;
025import java.util.Optional;
026import java.util.Set;
027
028import org.apache.avalon.framework.component.Component;
029import org.apache.avalon.framework.logger.AbstractLogEnabled;
030import org.apache.avalon.framework.service.ServiceException;
031import org.apache.avalon.framework.service.ServiceManager;
032import org.apache.avalon.framework.service.Serviceable;
033import org.apache.commons.io.IOUtils;
034import org.apache.commons.lang.IllegalClassException;
035import org.apache.excalibur.xml.sax.SAXParser;
036import org.apache.jackrabbit.util.Text;
037import org.xml.sax.InputSource;
038import org.xml.sax.SAXException;
039
040import org.ametys.core.observation.Event;
041import org.ametys.core.observation.ObservationManager;
042import org.ametys.core.right.RightManager;
043import org.ametys.core.ui.Callable;
044import org.ametys.core.user.CurrentUserProvider;
045import org.ametys.core.user.User;
046import org.ametys.core.user.UserIdentity;
047import org.ametys.core.user.UserManager;
048import org.ametys.core.util.DateUtils;
049import org.ametys.plugins.core.user.UserHelper;
050import org.ametys.plugins.explorer.ExplorerNode;
051import org.ametys.plugins.explorer.ModifiableExplorerNode;
052import org.ametys.plugins.explorer.ObservationConstants;
053import org.ametys.plugins.explorer.resources.ModifiableResourceCollection;
054import org.ametys.plugins.explorer.resources.actions.ExplorerResourcesDAO;
055import org.ametys.plugins.explorer.threads.jcr.JCRPost;
056import org.ametys.plugins.explorer.threads.jcr.JCRPostFactory;
057import org.ametys.plugins.explorer.threads.jcr.JCRThread;
058import org.ametys.plugins.explorer.threads.jcr.JCRThreadFactory;
059import org.ametys.plugins.explorer.threads.jcr.PostRichTextHandler;
060import org.ametys.plugins.repository.AmetysObject;
061import org.ametys.plugins.repository.AmetysObjectIterable;
062import org.ametys.plugins.repository.AmetysObjectResolver;
063import org.ametys.plugins.repository.AmetysRepositoryException;
064import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
065import org.ametys.plugins.repository.metadata.ModifiableRichText;
066
067/**
068 * Thread DAO
069 */
070public class ThreadDAO extends AbstractLogEnabled implements Serviceable, Component
071{
072    /** Avalon Role */
073    public static final String ROLE = ThreadDAO.class.getName();
074
075    /** Right to add a thread */
076    public static final String __RIGHTS_THREAD_ADD = "Plugin_Explorer_Thread_Add";
077
078    /** Right to edit a thread */
079    public static final String __RIGHTS_THREAD_EDIT = "Plugin_Explorer_Thread_Edit";
080
081    /** Right to delete a thread */
082    public static final String __RIGHTS_THREAD_DELETE = "Plugin_Explorer_Thread_Delete";
083
084    /** Right to add a post */
085    public static final String __RIGHTS_POST_ADD = "Plugin_Explorer_Post_Add";
086
087    /** Right to edit a post */
088    public static final String __RIGHTS_POST_EDIT = "Plugin_Explorer_Post_Edit";
089
090    /** Right to delete a post */
091    public static final String __RIGHTS_POST_DELETE = "Plugin_Explorer_Post_Delete";
092    
093    /** Explorer resources DAO */
094    protected ExplorerResourcesDAO _explorerResourcesDAO;
095    
096    /** Ametys resolver */
097    protected AmetysObjectResolver _resolver;
098    
099    /** Observer manager. */
100    protected ObservationManager _observationManager;
101    
102    /** The current user provider. */
103    protected CurrentUserProvider _currentUserProvider;
104    
105    /** User manager */
106    protected UserManager _userManager;
107    
108    /** The rights manager */
109    protected RightManager _rightManager;
110    
111    /** The user helper */
112    protected UserHelper _userHelper;
113
114    /** Avalon service manager */
115    protected ServiceManager _manager;
116    
117    
118    public void service(ServiceManager manager) throws ServiceException
119    {
120        _manager = manager;
121        _explorerResourcesDAO = (ExplorerResourcesDAO) manager.lookup(ExplorerResourcesDAO.ROLE);
122        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
123        _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE);
124        _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
125        _userManager = (UserManager) manager.lookup(UserManager.ROLE);
126        _rightManager = (RightManager) manager.lookup(RightManager.ROLE);
127        _userHelper = (UserHelper) manager.lookup(UserHelper.ROLE);
128    }
129
130    
131    /**
132     * Get thread info
133     * @param id The thread id
134     * @param includeChildren True to also include children
135     * @return the thread data in a map
136     */
137    @Callable
138    public Map<String, Object> getThreadData(String id, boolean includeChildren)
139    {
140        JCRThread thread = (JCRThread) _resolver.resolveById(id);
141        return getThreadData(thread, includeChildren);
142    }
143    
144    /**
145     * Get thread info
146     * @param thread The thread
147     * @param includeChildren True to also include children
148     * @return the thread data in a map
149     */
150    public Map<String, Object> getThreadData(JCRThread thread, boolean includeChildren)
151    {
152        Map<String, Object> result = new HashMap<>();
153        UserIdentity author = thread.getAuthor();
154        
155        result.put("id", thread.getId());
156        result.put("title", thread.getTitle());
157        result.put("description", thread.getDescription());
158        result.put("author", _formatAuthor(author));
159        result.put("authorLogin", author.getLogin());
160        result.put("authorPopulation", author.getPopulationId());
161        UserIdentity currentUser = _currentUserProvider.getUser();
162        result.put("isAuthor", author.equals(currentUser));
163        result.put("creationDate", DateUtils.dateToString(thread.getCreationDate()));
164        result.put("unreadPosts", thread.getUnreadPosts(currentUser));
165        Optional<AmetysObject> lastPost = thread.getChildren().stream().filter(child -> child instanceof JCRPost).max((post1, post2) -> ((JCRPost) post1).getCreationDate().compareTo(((JCRPost) post2).getCreationDate()));
166        JCRPost post = lastPost.isPresent() ? (JCRPost) lastPost.get() : null;
167        result.put("lastModificationDate", post != null ? post.getCreationDate() : thread.getCreationDate());
168        result.put("lastPostContent", post != null ? getPostContent(post) : null);
169        
170        Map<String, Object> rights = new HashMap<>();
171        
172        rights.put("threadEdit", _explorerResourcesDAO.getUserRight(currentUser, __RIGHTS_THREAD_EDIT, thread));
173        rights.put("threadDelete", _explorerResourcesDAO.getUserRight(currentUser, __RIGHTS_THREAD_DELETE, thread));
174        rights.put("postAdd", _explorerResourcesDAO.getUserRight(currentUser, __RIGHTS_POST_ADD, thread));
175        result.put("rights", rights);
176        
177        if (includeChildren)
178        {
179            List<Map<String, Object>> childrenData = new LinkedList<>();
180            result.put("posts", childrenData);
181            
182            AmetysObjectIterable<AmetysObject> children = thread.getChildren();
183            for (AmetysObject child : children)
184            {
185                if (child instanceof JCRPost)
186                {
187                    JCRPost jcrPost = (JCRPost) child;
188                    childrenData.add(getPostData(jcrPost, false, false));
189                }
190            }
191        }
192        
193        return result;
194    }
195    
196    /**
197     * Return the author of a thread in a formatted version ready to be displayed to the end user.
198     * @param userIdentity The author
199     * @return The formatted string
200     */
201    protected String _formatAuthor(UserIdentity userIdentity)
202    {
203        User user = _userManager.getUser(userIdentity.getPopulationId(), userIdentity.getLogin());
204        String name = user == null ? userIdentity.getLogin() : user.getFullName() + " (" + userIdentity.getLogin() + ")"; 
205        return name;
206    }
207    
208    /**
209     * Get post info
210     * @param ids The post ids
211     * @param fullInfo true to include full info (rights, parent id, etc...)
212     * @param isEdition true to get the content in edit mode
213     * @return the list of post data
214     */
215    @Callable
216    public List<Map<String, Object>> getPostsDataByIds(List<String> ids, boolean fullInfo, boolean isEdition)
217    {
218        List<JCRPost> posts = new LinkedList<>();
219        for (String id : ids)
220        {
221            posts.add((JCRPost) _resolver.resolveById(id));
222        }
223        
224        return getPostsData(posts, fullInfo, isEdition);
225    }
226    
227    /**
228     * Get post info
229     * @param posts The posts
230     * @param fullInfo true to include full info (rights, parent id, etc...)
231     * @param isEdition true to get the content in edit mode
232     * @return the list of post data
233     */
234    public List<Map<String, Object>> getPostsData(List<JCRPost> posts, boolean fullInfo, boolean isEdition)
235    {
236        List<Map<String, Object>> result = new LinkedList<>();
237        
238        for (JCRPost post : posts)
239        {
240            result.add(getPostData(post, fullInfo, isEdition));
241        }
242        
243        return result;
244    }
245    
246    /**
247     * Get post info
248     * @param id The post id
249     * @param fullInfo true to include full info (rights, parent id, etc...)
250     * @param isEdition true to get the content in edit mode
251     * @return the post data in a map
252     */
253    @Callable
254    public Map<String, Object> getPostDataById(String id, boolean fullInfo, boolean isEdition)
255    {
256        JCRPost post = (JCRPost) _resolver.resolveById(id);
257        return getPostData(post, fullInfo, isEdition);
258    }
259    
260    /**
261     * Get post info
262     * @param post The post
263     * @param fullInfo true to include full info (rights, parent id, etc...)
264     * @param isEdition true to get the content in edit mode
265     * @return the post data in a map
266     */
267    public Map<String, Object> getPostData(JCRPost post, boolean fullInfo, boolean isEdition)
268    {
269        Map<String, Object> result = new HashMap<>();
270        
271        UserIdentity author = post.getAuthor();
272        boolean isOwner = author.equals(_currentUserProvider.getUser());
273        
274        result.put("id", post.getId());
275        result.put("content", isEdition ? getPostContentForEditing(post) : getPostContent(post));
276        result.put("author", _userHelper.user2json(author));
277        result.put("isOwner", isOwner);
278        
279        result.put("creationDate", DateUtils.dateToString(post.getCreationDate()));
280        result.put("lastModifiedDate", DateUtils.dateToString(post.getLastModified()));
281        
282        result.put("canEdit", canEdit(post));
283        result.put("canDelete", canDelete(post));
284        
285        if (fullInfo)
286        {
287            result.putAll(_getPostDataFullInfo(post));
288        }
289        
290        return result;
291    }
292    
293    /**
294     * Determines if the post can be edited by current user
295     * @param post The post
296     * @return true if the post can be edited
297     */
298    protected boolean canEdit(JCRPost post)
299    {
300        boolean isOwner = post.getAuthor().equals(_currentUserProvider.getUser());
301        return isOwner || _explorerResourcesDAO.getUserRight(_currentUserProvider.getUser(), __RIGHTS_POST_EDIT, post);
302    }
303    
304    /**
305     * Determines if the post can be deleted by current user
306     * @param post The post
307     * @return true if the post can be deleted
308     */
309    protected boolean canDelete(JCRPost post)
310    {
311        boolean isOwner = post.getAuthor().equals(_currentUserProvider.getUser());
312        return isOwner || _explorerResourcesDAO.getUserRight(_currentUserProvider.getUser(), __RIGHTS_POST_DELETE, post);
313    }
314    
315    /**
316     * Convert the content of a post to a string (removing HTML tags)
317     * @param post The post
318     * @return the content of the post as string.
319     */
320    public String convertPostToString (JCRPost post)
321    {
322        SAXParser saxParser = null;
323        try
324        {
325            PostRichTextHandler txtHandler = new PostRichTextHandler();
326            saxParser = (SAXParser) _manager.lookup(SAXParser.ROLE);
327            saxParser.parse(new InputSource(post.getContent().getInputStream()), txtHandler);
328            return txtHandler.getValue().trim();
329        }
330        catch (ServiceException e)
331        {
332            getLogger().error("Unable to get a SAX parser", e);
333            return null;
334        }
335        catch (IOException | SAXException e)
336        {
337            getLogger().error("Cannot parse inputstream", e);
338            return null;
339        }
340        finally
341        {
342            _manager.release(saxParser);
343        }
344    }
345    
346    /**
347     * Retrieves the post additional info (rights, parent id, etc...)
348     * @param post The post
349     * @return the post additional info (rights, parent id, etc...) in a map
350     */
351    protected Map<String, Object> _getPostDataFullInfo(JCRPost post)
352    {
353        Map<String, Object> result = new HashMap<>();
354        
355        ExplorerNode explorerNode = post.getParent();
356        ExplorerNode root = explorerNode;
357        while (true)
358        {
359            if (root.getParent() instanceof ExplorerNode)
360            {
361                root = root.getParent();
362            }
363            else
364            {
365                break;
366            }
367        }
368        result.put("rootId", root.getId());
369        result.put("parentId", explorerNode.getId());
370        result.put("name", post.getName());
371        result.put("path", explorerNode.getExplorerPath());
372        result.put("isModifiable", true);
373        
374        result.put("rights", _getUserRights(explorerNode));
375        
376        return result;
377    }
378    
379    /**
380     * Get the user rights on the resource collection
381     * @param node The explorer node
382     * @return The user's rights
383     */
384    protected Set<String> _getUserRights(ExplorerNode node)
385    {
386        UserIdentity user = _currentUserProvider.getUser();
387        return _rightManager.getUserRights(user, node);
388    }
389    
390    /**
391     * Add a thread
392     * @param id The identifier of the parent in which the thread will be added
393     * @param inputName The desired name for the thread
394     * @param inputDescription The thread description
395     * @return The result map with id, parentId and name keys
396     * @throws IllegalAccessException If the user has no sufficient rights
397     */
398    @Callable
399    public Map<String, Object> addThread(String id, String inputName, String inputDescription) throws IllegalAccessException
400    {
401        Map<String, Object> result = new HashMap<>();
402        
403        String originalName = Text.escapeIllegalJcrChars(inputName);
404        String description = inputDescription.replaceAll("\\r\\n|\\r|\\n", "<br />");
405        assert id != null;
406        
407        AmetysObject object = _resolver.resolveById(id);
408        if (!(object instanceof ModifiableResourceCollection || object instanceof JCRThread))
409        {
410            throw new IllegalClassException(ModifiableResourceCollection.class, object.getClass());
411        }
412        
413        ModifiableTraversableAmetysObject parent = (ModifiableTraversableAmetysObject) object;
414        
415        // Check user right
416        _explorerResourcesDAO.checkUserRight(object, __RIGHTS_THREAD_ADD);
417        
418        if (!_explorerResourcesDAO.checkLock(parent))
419        {
420            getLogger().warn("User '" + _currentUserProvider.getUser() + "' try to modify thread '" + object.getName() + "' but it is locked by another user");
421            result.put("message", "locked");
422            return result;
423        }
424        
425        int index = 2;
426        String name = originalName;
427        while (parent.hasChild(name))
428        {
429            name = originalName + " (" + index + ")";
430            index++;
431        }
432        
433        JCRThread thread = parent.createChild(name, JCRThreadFactory.THREAD_NODETYPE);
434        thread.setTitle(inputName);
435        thread.setDescription(description);
436        thread.setAuthor(_currentUserProvider.getUser());
437        Date now = new Date();
438        thread.setCreationDate(now);
439        
440        parent.saveChanges();
441        
442        result.put("id", thread.getId());
443        result.put("parentId", id);
444        result.put("name", name);
445        
446        // Notify listeners
447        Map<String, Object> eventParams = new HashMap<>();
448        eventParams.put(ObservationConstants.ARGS_ID, thread.getId());
449        eventParams.put(ObservationConstants.ARGS_THREAD, thread);
450        _observationManager.notify(new Event(ObservationConstants.EVENT_THREAD_CREATED, _currentUserProvider.getUser(), eventParams));
451        
452        return result;
453    }
454    
455    /**
456     * Edit a thread
457     * @param id The identifier of the thread
458     * @param inputName The new name
459     * @param inputDescription The new description
460     * @return The result map with id and name keys
461     * @throws IllegalAccessException If the user has no sufficient rights
462     */
463    @Callable
464    public Map<String, Object> editThread(String id, String inputName, String inputDescription) throws IllegalAccessException
465    {
466        Map<String, Object> result = new HashMap<>();
467        
468        String description = inputDescription.replaceAll("\\r\\n|\\r|\\n", "<br />");
469        assert id != null;
470        
471        AmetysObject object = _resolver.resolveById(id);
472        if (!(object instanceof JCRThread))
473        {
474            throw new IllegalClassException(JCRThread.class, object.getClass());
475        }
476        
477        JCRThread thread = (JCRThread) object;
478        
479        // Check user right
480        if (!_currentUserProvider.getUser().equals(thread.getAuthor()))
481        {
482            _explorerResourcesDAO.checkUserRight(object, __RIGHTS_THREAD_EDIT);
483        }
484        
485        if (!_explorerResourcesDAO.checkLock(thread))
486        {
487            getLogger().warn("User '" + _currentUserProvider.getUser() + "' try to modify thread '" + object.getName() + "' but it is locked by another user");
488            result.put("message", "locked");
489            return result;
490        }
491        
492        String originalName = Text.escapeIllegalJcrChars(inputName);
493        String name = originalName;
494        if (!name.equals(thread.getName()))
495        {
496            ModifiableTraversableAmetysObject parent = (ModifiableTraversableAmetysObject) thread.getParent();
497            int index = 2;
498            while (parent.hasChild(name))
499            {
500                name = originalName + " (" + index + ")";
501                index++;
502            }
503            thread.rename(name);
504        }
505        
506        
507        thread.setTitle(inputName);
508        if (description != null)
509        {
510            thread.setDescription(description);
511        }
512        
513        thread.saveChanges();
514
515        result.put("id", thread.getId());
516        result.put("title", name);
517
518        // Notify listeners
519        Map<String, Object> eventParams = new HashMap<>();
520        eventParams.put(ObservationConstants.ARGS_ID, thread.getId());
521        eventParams.put(ObservationConstants.ARGS_THREAD, thread);
522        _observationManager.notify(new Event(ObservationConstants.EVENT_THREAD_UPDATED, _currentUserProvider.getUser(), eventParams));
523
524        return result;
525    }
526    
527    /**
528     * Rename a thread
529     * @param id The id of the thread
530     * @param name The thread name
531     * @return The result map with id, name and message keys
532     * @throws IllegalAccessException If the user has no sufficient rights
533     */
534    @Callable
535    public Map<String, Object> renameThread(String id, String name) throws IllegalAccessException
536    {
537        Map<String, Object> result = new HashMap<>();
538
539        assert id != null;
540        
541        AmetysObject object = _resolver.resolveById(id);
542        if (!(object instanceof JCRThread))
543        {
544            throw new IllegalClassException(JCRThread.class, object.getClass());
545        }
546        
547        JCRThread thread = (JCRThread) object;
548        
549        // Check user right
550        if (!_currentUserProvider.getUser().equals(thread.getAuthor()))
551        {
552            _explorerResourcesDAO.checkUserRight(object, __RIGHTS_THREAD_EDIT);
553        }
554        
555        if (!_explorerResourcesDAO.checkLock(thread))
556        {
557            getLogger().warn("User '" + _currentUserProvider.getUser() + "' try to modify thread '" + object.getName() + "' but it is locked by another user");
558            result.put("message", "locked");
559            return result;
560        }
561        
562        if (!name.equals(thread.getName()))
563        {
564            ModifiableTraversableAmetysObject parent = (ModifiableTraversableAmetysObject) thread.getParent();
565            if (parent.hasChild(name))
566            {
567                result.put("message", "already-exist");
568                return result;
569            }
570            thread.rename(name);
571        }
572        
573        thread.setTitle(name);
574        thread.saveChanges();
575
576        result.put("id", thread.getId());
577        result.put("name", name);
578
579        // Notify listeners
580        Map<String, Object> eventParams = new HashMap<>();
581        eventParams.put(ObservationConstants.ARGS_ID, thread.getId());
582        eventParams.put(ObservationConstants.ARGS_THREAD, thread);
583        _observationManager.notify(new Event(ObservationConstants.EVENT_THREAD_RENAMED, _currentUserProvider.getUser(), eventParams));
584
585        return result;
586    }
587    
588    /**
589     * Delete a thread
590     * @param id The id of the thread
591     * @return The result map with id, parent id and message keys
592     * @throws IllegalAccessException If the user has no sufficient rights
593     */
594    @Callable
595    public Map<String, Object> deleteThread(String id) throws IllegalAccessException
596    {
597        Map<String, Object> result = new HashMap<>();
598
599        assert id != null;
600        
601        AmetysObject object = _resolver.resolveById(id);
602        if (!(object instanceof JCRThread))
603        {
604            throw new IllegalClassException(JCRThread.class, object.getClass());
605        }
606        
607        JCRThread thread = (JCRThread) object;
608        
609        // Check user right
610        if (!_currentUserProvider.getUser().equals(thread.getAuthor()))
611        {
612            _explorerResourcesDAO.checkUserRight(object, __RIGHTS_THREAD_DELETE);
613        }
614        
615        if (!_explorerResourcesDAO.checkLock(thread))
616        {
617            getLogger().warn("User '" + _currentUserProvider.getUser() + "' try to modify thread '" + object.getName() + "' but it is locked by another user");
618            result.put("message", "locked");
619            return result;
620        }
621        
622        ModifiableExplorerNode parent = thread.getParent();
623        String parentId = parent.getId();
624        String name = thread.getName();
625        String path = thread.getPath();
626        
627        thread.remove();
628        parent.saveChanges();
629
630        result.put("id", id);
631        result.put("parentId", parentId);
632     
633        // Notify listeners
634        Map<String, Object> eventParams = new HashMap<>();
635        eventParams.put(ObservationConstants.ARGS_ID, id);
636        eventParams.put(ObservationConstants.ARGS_PARENT_ID, parentId);
637        eventParams.put(ObservationConstants.ARGS_NAME, name);
638        eventParams.put(ObservationConstants.ARGS_PATH, path);
639        _observationManager.notify(new Event(ObservationConstants.EVENT_THREAD_DELETED, _currentUserProvider.getUser(), eventParams));
640        
641        return result;
642    }
643    
644    /**
645     * Add a post
646     * @param threadId The identifier of the thread in which the post will be added
647     * @param inputContent The post content
648     * @return The result map with id, parentId and message keys
649     * @throws IllegalAccessException If the user has no sufficient rights
650     * @throws IOException If an error occurs
651     */
652    @Callable
653    public Map<String, Object> addPost(String threadId, String inputContent) throws IllegalAccessException, IOException
654    {
655        Map<String, Object> result = new HashMap<>();
656        
657        String name = JCRPostFactory.POST_NODENAME;
658        assert threadId != null;
659        
660        AmetysObject object = _resolver.resolveById(threadId);
661        if (!(object instanceof JCRThread))
662        {
663            throw new IllegalClassException(JCRThread.class, object.getClass());
664        }
665        
666        JCRThread parent = (JCRThread) object;
667        
668        // Check user right
669        if (!_currentUserProvider.getUser().equals(parent.getAuthor()))
670        {
671            _explorerResourcesDAO.checkUserRight(object.getParent(), __RIGHTS_POST_ADD);
672        }
673        
674        if (!_explorerResourcesDAO.checkLock(parent))
675        {
676            getLogger().warn("User '" + _currentUserProvider.getUser() + "' try to add a post '" + parent.getName() + "' but thread is locked by another user");
677            result.put("message", "locked");
678            return result;
679        }
680        
681        JCRPost post = parent.createChild(name, JCRPostFactory.POST_NODETYPE);
682        
683        setPostContent(post, inputContent);
684        
685        post.setAuthor(_currentUserProvider.getUser());
686        Date now = new Date();
687        post.setCreationDate(now);
688        post.setLastModified(now);
689        
690        parent.markAsRead(_currentUserProvider.getUser());
691        
692        parent.saveChanges();
693        
694        result.put("id", post.getId());
695        result.put("parentId", threadId);
696        result.put("name", name);
697        
698        // Notify listeners
699        Map<String, Object> eventParams = new HashMap<>();
700        eventParams.put(ObservationConstants.ARGS_ID, post.getId());
701        eventParams.put(ObservationConstants.ARGS_THREAD, parent);
702        eventParams.put(ObservationConstants.ARGS_POST, post);
703        _observationManager.notify(new Event(ObservationConstants.EVENT_THREAD_POST_CREATED, _currentUserProvider.getUser(), eventParams));
704        
705        return result;
706    }
707    
708    /**
709     * Update the content of a post
710     * @param post The post to update
711     * @param content The content as string
712     */
713    protected void setPostContent(JCRPost post, String content)
714    {
715        try
716        {
717            ModifiableRichText richText = post.getContent();
718            
719            richText.setMimeType("text/plain");
720            richText.setLastModified(new Date());
721            richText.setInputStream(new ByteArrayInputStream(content.getBytes("UTF-8")));
722        }
723        catch (IOException e)
724        {
725            throw new AmetysRepositoryException("Failed to set post rich text", e);
726        }
727    }
728    
729    /**
730     * Get the content of a post as a String
731     * @param post the post
732     * @return The content as String
733     * @throws AmetysRepositoryException if failed to parse content
734     */
735    protected String getPostContent(JCRPost post) throws AmetysRepositoryException
736    {
737        try
738        {
739            ModifiableRichText richText = post.getContent();
740            return IOUtils.toString(richText.getInputStream(), "UTF-8");
741        }
742        catch (IOException e)
743        {
744            throw new AmetysRepositoryException("Failed to get post rich text", e);
745        }
746    }
747    
748    /**
749     * Get the content of a post to edit as a String
750     * @param post the post
751     * @return The content as String
752     * @throws AmetysRepositoryException if failed to parse content
753     */
754    protected String getPostContentForEditing(JCRPost post) throws AmetysRepositoryException
755    {
756        return getPostContent(post);
757    }
758    
759    /**
760     * Edit a post
761     * @param id The identifier of the post
762     * @param inputContent The post content
763     * @return The result map with id, parentId and message keys
764     * @throws IllegalAccessException If the user has no sufficient rights
765     * @throws IOException If an error occurs
766     */
767    @Callable
768    public Map<String, Object> editPost(String id, String inputContent) throws IllegalAccessException, IOException
769    {
770        Map<String, Object> result = new HashMap<>();
771        
772        assert id != null;
773        
774        AmetysObject object = _resolver.resolveById(id);
775        if (!(object instanceof JCRPost))
776        {
777            throw new IllegalClassException(JCRPost.class, object.getClass());
778        }
779        
780        JCRPost post = (JCRPost) object;
781        
782        // Check user right
783        if (!_currentUserProvider.getUser().equals(post.getAuthor()))
784        {
785            _explorerResourcesDAO.checkUserRight(object.getParent(), __RIGHTS_POST_EDIT);
786        }
787                
788        if (!_explorerResourcesDAO.checkLock(post))
789        {
790            getLogger().warn("User '" + _currentUserProvider.getUser() + "' try to modify post '" + object.getName() + "' but it is locked by another user");
791            result.put("message", "locked");
792            return result;
793        }
794        
795        setPostContent(post, inputContent);
796        
797        Date now = new Date();
798        post.setLastModified(now);
799        
800        post.saveChanges();
801
802        result.put("id", post.getId());
803        result.put("content", getPostContent(post));
804        
805        // Notify listeners
806        Map<String, Object> eventParams = new HashMap<>();
807        eventParams.put(ObservationConstants.ARGS_ID, post.getId());
808        eventParams.put(ObservationConstants.ARGS_THREAD, post.getParent());
809        eventParams.put(ObservationConstants.ARGS_POST, post);
810        _observationManager.notify(new Event(ObservationConstants.EVENT_THREAD_POST_UPDATED, _currentUserProvider.getUser(), eventParams));
811        
812        return result;
813    }
814    
815    /**
816     * Delete a post
817     * @param id The id of the post
818     * @return The result map with id, parent id and message keys
819     * @throws IllegalAccessException If the user has no sufficient rights
820     */
821    @Callable
822    public Map<String, Object> deletePost(String id) throws IllegalAccessException
823    {
824        Map<String, Object> result = new HashMap<>();
825
826        assert id != null;
827        
828        AmetysObject object = _resolver.resolveById(id);
829        if (!(object instanceof JCRPost))
830        {
831            throw new IllegalClassException(JCRPost.class, object.getClass());
832        }
833        
834        JCRPost post = (JCRPost) object;
835        
836        // Check user right
837        if (!_currentUserProvider.getUser().equals(post.getAuthor()))
838        {
839            _explorerResourcesDAO.checkUserRight(object.getParent(), __RIGHTS_POST_DELETE);
840        }
841        
842        if (!_explorerResourcesDAO.checkLock(post))
843        {
844            getLogger().warn("User '" + _currentUserProvider.getUser() + "' try to delete thread '" + object.getName() + "' but it is locked by another user");
845            result.put("message", "locked");
846            return result;
847        }
848        
849        ModifiableExplorerNode parent = post.getParent();
850        String parentId = parent.getId();
851        String name = post.getName();
852        String path = post.getPath();
853        
854        post.remove();
855        parent.saveChanges();
856
857        result.put("id", id);
858        result.put("parentId", parentId);
859     
860        // Notify listeners
861        Map<String, Object> eventParams = new HashMap<>();
862        eventParams.put(ObservationConstants.ARGS_ID, id);
863        eventParams.put(ObservationConstants.ARGS_THREAD, parent);
864        eventParams.put(ObservationConstants.ARGS_NAME, name);
865        eventParams.put(ObservationConstants.ARGS_PATH, path);
866        _observationManager.notify(new Event(ObservationConstants.EVENT_THREAD_POST_DELETED, _currentUserProvider.getUser(), eventParams));
867        
868        return result;
869    }
870    
871    /**
872     * Mark a thread as read by the current user
873     * @param id The thread id
874     * @return The result map with id, parent id and message keys
875     */
876    @Callable
877    public Map<String, Object> markAsRead(String id)
878    {
879        Map<String, Object> result = new HashMap<>();
880        
881        assert id != null;
882        
883        AmetysObject object = _resolver.resolveById(id);
884        if (!(object instanceof JCRThread))
885        {
886            throw new IllegalClassException(JCRThread.class, object.getClass());
887        }
888        
889        JCRThread thread = (JCRThread) object;
890        
891        if (!_explorerResourcesDAO.checkLock(thread))
892        {
893            getLogger().warn("User '" + _currentUserProvider.getUser() + "' try to modify thread '" + object.getName() + "' but it is locked by another user");
894            result.put("message", "locked");
895            return result;
896        }
897        
898        UserIdentity user = _currentUserProvider.getUser();
899        thread.markAsRead(user);
900        
901        thread.saveChanges();
902
903        result.put("id", thread.getId());
904        return result;
905    }
906    
907}