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