001/*
002 *  Copyright 2016 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.resources.actions;
017
018import java.io.ByteArrayInputStream;
019import java.io.IOException;
020import java.io.InputStream;
021import java.io.Reader;
022import java.nio.charset.StandardCharsets;
023import java.time.LocalDateTime;
024import java.time.format.DateTimeFormatter;
025import java.util.ArrayList;
026import java.util.Collection;
027import java.util.Collections;
028import java.util.Comparator;
029import java.util.Date;
030import java.util.HashMap;
031import java.util.HashSet;
032import java.util.LinkedList;
033import java.util.List;
034import java.util.Map;
035import java.util.Objects;
036import java.util.Set;
037
038import javax.jcr.Node;
039import javax.jcr.RepositoryException;
040import javax.jcr.Session;
041import javax.jcr.lock.Lock;
042import javax.jcr.lock.LockManager;
043
044import org.apache.avalon.framework.component.Component;
045import org.apache.avalon.framework.context.ContextException;
046import org.apache.avalon.framework.context.Contextualizable;
047import org.apache.avalon.framework.service.ServiceException;
048import org.apache.avalon.framework.service.ServiceManager;
049import org.apache.avalon.framework.service.Serviceable;
050import org.apache.cocoon.Constants;
051import org.apache.cocoon.ProcessingException;
052import org.apache.cocoon.environment.Context;
053import org.apache.commons.io.IOUtils;
054import org.apache.commons.io.output.NullOutputStream;
055import org.apache.commons.lang.IllegalClassException;
056import org.apache.commons.lang.StringUtils;
057import org.apache.jackrabbit.util.Text;
058import org.apache.tika.metadata.Metadata;
059
060import org.ametys.core.observation.Event;
061import org.ametys.core.observation.ObservationManager;
062import org.ametys.core.right.RightManager;
063import org.ametys.core.right.RightManager.RightResult;
064import org.ametys.core.ui.Callable;
065import org.ametys.core.user.CurrentUserProvider;
066import org.ametys.core.user.UserIdentity;
067import org.ametys.core.util.DateUtils;
068import org.ametys.core.util.I18nUtils;
069import org.ametys.plugins.core.user.UserHelper;
070import org.ametys.plugins.explorer.ExplorerNode;
071import org.ametys.plugins.explorer.ModifiableExplorerNode;
072import org.ametys.plugins.explorer.ObservationConstants;
073import org.ametys.plugins.explorer.applications.ExplorerApplication;
074import org.ametys.plugins.explorer.applications.ExplorerApplicationExtensionPoint;
075import org.ametys.plugins.explorer.calendars.Calendar;
076import org.ametys.plugins.explorer.cmis.CMISRootResourcesCollection;
077import org.ametys.plugins.explorer.cmis.CMISTreeFactory;
078import org.ametys.plugins.explorer.resources.CommentableResource;
079import org.ametys.plugins.explorer.resources.ModifiableResource;
080import org.ametys.plugins.explorer.resources.ModifiableResourceCollection;
081import org.ametys.plugins.explorer.resources.Resource;
082import org.ametys.plugins.explorer.resources.ResourceCollection;
083import org.ametys.plugins.explorer.resources.generators.ResourcesExplorerGenerator;
084import org.ametys.plugins.explorer.resources.jcr.JCRResource;
085import org.ametys.plugins.explorer.resources.jcr.JCRResourcesCollectionFactory;
086import org.ametys.plugins.explorer.resources.metadata.TikaProvider;
087import org.ametys.plugins.explorer.resources.metadata.populate.ResourceMetadataPopulator;
088import org.ametys.plugins.explorer.resources.metadata.populate.ResourceMetadataPopulatorExtensionPoint;
089import org.ametys.plugins.explorer.threads.actions.ThreadDAO;
090import org.ametys.plugins.explorer.threads.jcr.JCRPost;
091import org.ametys.plugins.explorer.threads.jcr.JCRPostFactory;
092import org.ametys.plugins.explorer.threads.jcr.JCRThread;
093import org.ametys.plugins.repository.AmetysObject;
094import org.ametys.plugins.repository.AmetysObjectIterable;
095import org.ametys.plugins.repository.AmetysObjectResolver;
096import org.ametys.plugins.repository.AmetysRepositoryException;
097import org.ametys.plugins.repository.ModifiableAmetysObject;
098import org.ametys.plugins.repository.RemovableAmetysObject;
099import org.ametys.plugins.repository.RepositoryConstants;
100import org.ametys.plugins.repository.RepositoryIntegrityViolationException;
101import org.ametys.plugins.repository.TraversableAmetysObject;
102import org.ametys.plugins.repository.UnknownAmetysObjectException;
103import org.ametys.plugins.repository.dublincore.DublinCoreAwareAmetysObject;
104import org.ametys.plugins.repository.jcr.JCRAmetysObject;
105import org.ametys.plugins.repository.jcr.JCRTraversableAmetysObject;
106import org.ametys.plugins.repository.lock.LockHelper;
107import org.ametys.plugins.repository.lock.LockableAmetysObject;
108import org.ametys.plugins.repository.metadata.ModifiableRichText;
109import org.ametys.plugins.repository.version.VersionableAmetysObject;
110import org.ametys.runtime.i18n.I18nizableText;
111import org.ametys.runtime.plugin.component.AbstractLogEnabled;
112
113/**
114 * Explorer resources DAO
115 */
116public class ExplorerResourcesDAO extends AbstractLogEnabled implements Serviceable, Component, Contextualizable
117{
118    /** Avalon Role */
119    public static final String ROLE = ExplorerResourcesDAO.class.getName();
120
121    /** Right id to unlock all resources */
122    public static final String RIGHTS_RESOURCE_UNLOCK_ALL = "Plugin_Explorer_File_Unlock_All";
123
124    /** Right id to add a resource */
125    public static final String RIGHTS_RESOURCE_ADD = "Plugin_Explorer_File_Add";
126
127    /** Right id to rename a resource */
128    public static final String RIGHTS_RESOURCE_RENAME = "Plugin_Explorer_File_Rename";
129
130    /** Right id to delete a resource */
131    public static final String RIGHTS_RESOURCE_DELETE = "Plugin_Explorer_File_Delete";
132
133    /** Right id to edit DC metadata of a resource */
134    public static final String RIGHTS_RESOURCE_EDIT_DC = "Plugin_Explorer_File_Edit_DC_Metadata";
135
136    /** Right id to comment a resource */
137    public static final String RIGHTS_RESOURCE_COMMENT = "Plugin_Explorer_File_Comment";
138
139    /** Right id to moderate comment a resource */
140    public static final String RIGHTS_RESOURCE_MODERATE_COMMENT = "Plugin_Explorer_File_Moderate_Comments";
141
142    /** Right id to add a folder */
143    public static final String RIGHTS_COLLECTION_ADD = "Plugin_Explorer_Folder_Add";
144
145    /** Right id to edit a folder */
146    public static final String RIGHTS_COLLECTION_EDIT = "Plugin_Explorer_Folder_Edit";
147
148    /** Right id to delete a folder */
149    public static final String RIGHTS_COLLECTION_DELETE = "Plugin_Explorer_Folder_Delete";
150
151    /** Ametys resolver */
152    protected AmetysObjectResolver _resolver;
153
154    /** The rights manager */
155    protected RightManager _rightManager;
156
157    /** Observer manager. */
158    protected ObservationManager _observationManager;
159
160    /** The current user provider. */
161    protected CurrentUserProvider _currentUserProvider;
162
163    /** I18n utils */
164    protected I18nUtils _i18nUtils;
165
166    /** The avalon context */
167    protected org.apache.avalon.framework.context.Context _context;
168
169    /** The cocoon context */
170    protected Context _cocoonContext;
171
172    /** The tika provider. */
173    protected TikaProvider _tikaProvider;
174
175    /** The metadata populator extension point. */
176    protected ResourceMetadataPopulatorExtensionPoint _metadataPopulatorEP;
177
178    /** The users manager */
179    protected UserHelper _userHelper;
180
181    /** The explorer application EP */
182    protected ExplorerApplicationExtensionPoint _explorerApplicationEP;
183
184    /** The thread DAO */
185    protected ThreadDAO _threadDAO;
186
187    @Override
188    public void service(ServiceManager manager) throws ServiceException
189    {
190        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
191        _rightManager = (RightManager) manager.lookup(RightManager.ROLE);
192        _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE);
193        _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
194        _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE);
195        _tikaProvider = (TikaProvider) manager.lookup(TikaProvider.ROLE);
196        _metadataPopulatorEP = (ResourceMetadataPopulatorExtensionPoint) manager.lookup(ResourceMetadataPopulatorExtensionPoint.ROLE);
197        _userHelper = (UserHelper) manager.lookup(UserHelper.ROLE);
198        _explorerApplicationEP = (ExplorerApplicationExtensionPoint) manager.lookup(ExplorerApplicationExtensionPoint.ROLE);
199        _threadDAO = (ThreadDAO) manager.lookup(ThreadDAO.ROLE);
200    }
201
202    @Override
203    public void contextualize(org.apache.avalon.framework.context.Context context) throws ContextException
204    {
205        _context = context;
206        _cocoonContext = (Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT);
207    }
208
209    /**
210     * Get the root nodes for resources
211     * 
212     * @return the root nodes
213     */
214    public List<ExplorerNode> getResourcesRootNodes()
215    {
216        List<ExplorerNode> roots = new ArrayList<>();
217        roots.add(_resolver.resolveByPath(RepositoryConstants.NAMESPACE_PREFIX + ":resources"));
218        return roots;
219    }
220
221    /**
222     * Retrieves the set of standard data for the explorer root node
223     * 
224     * @return The data structured in a map
225     */
226    @Callable
227    public List<Map<String, Object>> getRootNodesInfo()
228    {
229        List<Map<String, Object>> infos = new ArrayList<>();
230
231        List<ExplorerNode> rootNodes = getResourcesRootNodes();
232        for (ExplorerNode rootNode : rootNodes)
233        {
234            infos.add(getDefaultInfoAsRootNode(rootNode));
235        }
236        return infos;
237    }
238
239    /**
240     * Get the necessary default info for a root node. Can be used to construct
241     * a root node for a tree (client side).
242     * 
243     * @param id The root node id
244     * @return A map which contains a set of default info (such as id,
245     *         applicationId, path, type etc...)
246     */
247    @Callable
248    public Map<String, Object> getDefaultInfoAsRootNode(String id)
249    {
250        ExplorerNode node = _resolver.resolveById(id);
251        return getDefaultInfoAsRootNode(node);
252    }
253
254    /**
255     * Get the necessary default info for a root node. Can be used to construct
256     * a root node for a tree (client side).
257     * 
258     * @param rootNode The root node
259     * @return A map which contains a set of default info (such as id,
260     *         applicationId, path, type etc...)
261     */
262    public Map<String, Object> getDefaultInfoAsRootNode(ExplorerNode rootNode)
263    {
264        Map<String, Object> result = new HashMap<>();
265
266        result.put("id", rootNode.getId());
267        result.put("applicationId", rootNode.getApplicationId());
268        result.put("name", "resources");
269        result.put("cls", "root");
270        result.put("iconCls", "ametysicon-folder249");
271
272        result.put("text", _i18nUtils.translate(new I18nizableText("plugin.explorer", "PLUGINS_EXPLORER_ROOT_NODE")));
273
274        result.put("path", "/dummy/resources");
275        result.put("type", ResourcesExplorerGenerator.RESOURCE_COLLECTION);
276
277        boolean hasResources = false;
278        if (rootNode instanceof ResourceCollection)
279        {
280            hasResources = ((ResourceCollection) rootNode).hasChildResources();
281        }
282        boolean hasChildNodes = rootNode.hasChildExplorerNodes();
283
284        if (hasChildNodes)
285        {
286            result.put("hasChildNodes", true);
287        }
288
289        if (hasResources)
290        {
291            result.put("hasResources", true);
292        }
293
294        result.put("isModifiable", false);
295
296        if (rootNode instanceof ModifiableExplorerNode)
297        {
298            result.put("canCreateChild", true);
299        }
300
301        return result;
302    }
303
304    /**
305     * Get the informations on given nodes (resources or collections)
306     * 
307     * @param ids The ids of node
308     * @return the nodes information
309     */
310    @Callable
311    public Map<String, Object> getNodesInfo(List<String> ids)
312    {
313        List<Map<String, Object>> objects = new ArrayList<>();
314        List<String> objectsNotFound = new ArrayList<>();
315
316        for (String id : ids)
317        {
318            try
319            {
320                AmetysObject ao = _resolver.resolveById(id);
321
322                if (ao instanceof ExplorerNode)
323                {
324                    objects.add(getExplorerNodeProperties((ExplorerNode) ao));
325                }
326                else if (ao instanceof Resource)
327                {
328                    objects.add(getResourceProperties((Resource) ao));
329                }
330            }
331            catch (UnknownAmetysObjectException e)
332            {
333                objectsNotFound.add(id);
334            }
335        }
336
337        Map<String, Object> result = new HashMap<>();
338        result.put("objects", objects);
339        result.put("objectsNotFound", objectsNotFound);
340
341        return result;
342    }
343
344    /**
345     * Get the explorer node properties
346     * 
347     * @param node The explorer node
348     * @return The properties
349     */
350    public Map<String, Object> getExplorerNodeProperties(ExplorerNode node)
351    {
352        Map<String, Object> infos = new HashMap<>();
353
354        ExplorerNode root = node;
355        AmetysObject parent = null;
356
357        while (true)
358        {
359            parent = root.getParent();
360            if (parent instanceof ExplorerNode)
361            {
362                root = (ExplorerNode) parent;
363            }
364            else
365            {
366                break;
367            }
368        }
369
370        parent = node.getParent();
371
372        infos.put("rootId", root.getId());
373        infos.put("rootOwnerType", "explorer");
374        infos.put("parentId", parent instanceof ExplorerNode ? parent.getId() : null);
375        infos.put("id", node.getId());
376        infos.put("applicationId", node.getApplicationId());
377        infos.put("name", node.getName());
378        infos.put("path", node.getExplorerPath());
379        infos.put("isModifiable", node instanceof ModifiableAmetysObject);
380        infos.put("canCreateChild", node instanceof ModifiableExplorerNode);
381
382        infos.put("rights", getUserRights(node));
383
384        // Adding attributs specific to applications
385        String applicationId = node.getApplicationId();
386        ExplorerApplication explorerApplication = _explorerApplicationEP.hasExtension(applicationId) ? _explorerApplicationEP.getExtension(applicationId) : null;
387        if (explorerApplication != null)
388        {
389            Map<String, String> additionalNodeParameters = explorerApplication.getAdditionalNodeParameters(node);
390            if (additionalNodeParameters != null)
391            {
392                additionalNodeParameters.entrySet().stream().forEach(entry -> infos.put(entry.getKey(), entry.getValue()));
393            }
394        }
395
396        return infos;
397    }
398
399    /**
400     * Get the resource properties
401     * 
402     * @param resource The resources
403     * @return The properties
404     */
405    public Map<String, Object> getResourceProperties(Resource resource)
406    {
407        Map<String, Object> infos = new HashMap<>();
408
409        ResourceCollection parentAO = resource.getParent();
410        ResourceCollection root = parentAO;
411        while (true)
412        {
413            if (root.getParent() instanceof ResourceCollection)
414            {
415                root = root.getParent();
416            }
417            else
418            {
419                break;
420            }
421        }
422        infos.put("id", resource.getId());
423        infos.put("rootId", root.getId());
424        infos.put("rootOwnerType", "explorer");
425        infos.put("parentId", parentAO.getId());
426        infos.put("name", resource.getName());
427        infos.put("path", resource.getResourcePath());
428        infos.put("isModifiable", resource instanceof ModifiableAmetysObject);
429
430        infos.put("rights", getUserRights(parentAO));
431
432        return infos;
433    }
434
435    /**
436     * Get the path of pages which match filter regexp
437     * @param id The id of explorer node to start search
438     * @param value the value to match
439     * @param allowedExtensions The allowed file extensions (lower-case). Can be null or empty to not filter on file extensions
440     * @return the matching paths
441     */
442    @Callable
443    public List<String> filterResourcesByRegExp(String id, String value, List<String> allowedExtensions)
444    {
445        List<String> matchingPaths = new ArrayList<>();
446
447        ExplorerNode root = _resolver.resolveById(id);
448        
449        String toMatch = org.apache.commons.lang3.StringUtils.stripAccents(value.toLowerCase()).trim();
450        
451        if (root instanceof TraversableAmetysObject)
452        {
453            TraversableAmetysObject traversableObject = (TraversableAmetysObject) root;
454            AmetysObjectIterable< ? extends AmetysObject> children = traversableObject.getChildren();
455            for (AmetysObject ao : children)
456            {
457                if (ao instanceof Resource)
458                {
459                    _getMatchingResource((Resource) ao, toMatch, allowedExtensions, matchingPaths);
460                }
461                else if (ao instanceof ExplorerNode)
462                {
463                    _getMatchingExplorerNode ((ExplorerNode) ao, toMatch, allowedExtensions, matchingPaths);
464                }
465            }
466        }
467        
468        return matchingPaths;
469    }
470    
471    private void _getMatchingExplorerNode(ExplorerNode explorerNode, String value, List<String> allowedExtensions, List<String> matchingPaths)
472    {
473        String title =  org.apache.commons.lang3.StringUtils.stripAccents(explorerNode.getName().toLowerCase());
474        
475        if (title.contains(value))
476        {
477            matchingPaths.add(explorerNode.getExplorerPath());
478        }
479        
480        if (explorerNode instanceof TraversableAmetysObject)
481        {
482            TraversableAmetysObject traversableObject = (TraversableAmetysObject) explorerNode;
483            
484            AmetysObjectIterable< ? extends AmetysObject> children = traversableObject.getChildren();
485            for (AmetysObject ao : children)
486            {
487                if (ao instanceof Resource)
488                {
489                    _getMatchingResource((Resource) ao, value, allowedExtensions, matchingPaths);
490                }
491                else if (ao instanceof ExplorerNode)
492                {
493                    _getMatchingExplorerNode ((ExplorerNode) ao, value, allowedExtensions, matchingPaths);
494                }
495            }
496        }
497    }
498    
499    private void _getMatchingResource(Resource resource, String value, List<String> allowedExtensions, List<String> matchingPaths)
500    {
501        String filename =  org.apache.commons.lang3.StringUtils.stripAccents(resource.getName().toLowerCase());
502        String fileExtension = filename.lastIndexOf(".") > 0 ? filename.substring(filename.lastIndexOf(".") + 1) : "";
503        if (filename.contains(value) && (allowedExtensions == null || allowedExtensions.size() == 0 || allowedExtensions.contains(fileExtension)))
504        {
505            matchingPaths.add(resource.getResourcePath());
506        }
507    }
508    
509    /**
510     * Get the user rights on the resource collection
511     * 
512     * @param node The explorer node
513     * @return The user's rights
514     */
515    protected Set<String> getUserRights(ExplorerNode node)
516    {
517        return _rightManager.getUserRights(_currentUserProvider.getUser(), node);
518    }
519
520    /**
521     * Check current user right on given explorer node
522     * 
523     * @param id The id of the explorer node
524     * @param rightId The if of right to check
525     * @return true if user has right
526     */
527    @Callable
528    public boolean hasRight(String id, String rightId)
529    {
530        UserIdentity user = _currentUserProvider.getUser();
531        ExplorerNode node = _resolver.resolveById(id);
532
533        return _rightManager.hasRight(user, rightId, node) == RightResult.RIGHT_ALLOW;
534    }
535
536    /**
537     * Check lock on a Ametys object
538     * 
539     * @param ao the Ametys object
540     * @return <code>false</code> if the Ametys object is locked and can not be
541     *         edited or deleted
542     */
543    public boolean checkLock(AmetysObject ao)
544    {
545        if (ao instanceof LockableAmetysObject)
546        {
547            LockableAmetysObject lockableAO = (LockableAmetysObject) ao;
548            if (lockableAO.isLocked())
549            {
550                if (!LockHelper.isLockOwner(lockableAO, _currentUserProvider.getUser()))
551                {
552                    return false;
553                }
554
555                if (ao instanceof JCRAmetysObject)
556                {
557                    _addLockToken(((JCRAmetysObject) ao).getNode());
558                }
559            }
560
561        }
562
563        return true;
564    }
565
566    private void _addLockToken(Node node)
567    {
568        try
569        {
570            if (node.isLocked())
571            {
572                LockManager lockManager = node.getSession().getWorkspace().getLockManager();
573
574                Lock lock = lockManager.getLock(node.getPath());
575                Node lockHolder = lock.getNode();
576
577                lockManager.addLockToken(lockHolder.getProperty(RepositoryConstants.METADATA_LOCKTOKEN).getString());
578            }
579        }
580        catch (RepositoryException e)
581        {
582            throw new AmetysRepositoryException("Unable to add lock token", e);
583        }
584    }
585
586    /**
587     * Check the user privilege on object
588     * 
589     * @param object the object
590     * @param rightId the right id
591     * @throws IllegalAccessException if the user has no sufficient rights
592     */
593    public void checkUserRight(AmetysObject object, String rightId) throws IllegalAccessException
594    {
595        ExplorerNode node;
596        if (object instanceof Resource)
597        {
598            node = object.getParent();
599        }
600        else
601        {
602            node = (ExplorerNode) object;
603        }
604
605        if (_rightManager.hasRight(_currentUserProvider.getUser(), rightId, object) != RightResult.RIGHT_ALLOW)
606        {
607            throw new IllegalAccessException("User '" + _currentUserProvider.getUser() + "' tried to access a privilege feature without convenient right [" + rightId
608                    + ", /resources" + node.getExplorerPath() + "]");
609        }
610    }
611
612    /**
613     * Retrieve the rights for the user
614     * 
615     * @param user The user
616     * @param right The right
617     * @param object The object
618     * @return True if the user has the right
619     */
620    public boolean getUserRight(UserIdentity user, String right, AmetysObject object)
621    {
622        AmetysObject explorerNode = object;
623        while (explorerNode != null && !(explorerNode instanceof ExplorerNode))
624        {
625            explorerNode = explorerNode.getParent();
626        }
627        if (explorerNode == null)
628        {
629            return false;
630        }
631        return _rightManager.hasRight(user, right, explorerNode) == RightResult.RIGHT_ALLOW;
632    }
633
634    /**
635     * Add a resource collection
636     * 
637     * @param parentId The identifier of the parent in which the resource
638     *            collection will be added
639     * @param desiredName The desired name for the resource collection
640     * @param renameIfExists If false, in case of existing name the resource
641     *            collection will not be created, if true it will be created and
642     *            renamed
643     * @return The result map with id, parentId, name and message keys
644     */
645    @Callable
646    public Map<String, Object> addResourceCollection(String parentId, String desiredName, Boolean renameIfExists)
647    {
648        Map<String, Object> result = new HashMap<>();
649
650        assert parentId != null;
651
652        AmetysObject object = _resolver.resolveById(parentId);
653        if (!(object instanceof ModifiableResourceCollection))
654        {
655            throw new IllegalClassException(ModifiableResourceCollection.class, object.getClass());
656        }
657
658        List<String> errors = new LinkedList<>();
659        ResourceCollection rc = addResourceCollection((ModifiableResourceCollection) object, desiredName, renameIfExists, errors);
660
661        if (!errors.isEmpty())
662        {
663            result.put("message", errors.get(0));
664        }
665        else
666        {
667            result.put("id", rc.getId());
668            result.put("parentID", parentId);
669            result.put("name", rc.getName());
670        }
671
672        return result;
673    }
674
675    /**
676     * Add a resource collection
677     * 
678     * @param parent The parent collection in which the resource collection will
679     *            be added
680     * @param desiredName The desired name for the resource collection
681     * @param renameIfExists If false, in case of existing name the resource
682     *            collection will not be created, if true it will be created and
683     *            renamed
684     * @param errors An optional list of possible error messages in case the
685     *            creation failed. Possible values are: locked, already-exist.
686     * @return The created resource collection or null if creation failed
687     */
688    public ResourceCollection addResourceCollection(ModifiableResourceCollection parent, String desiredName, Boolean renameIfExists, List<String> errors)
689    {
690        String originalName = desiredName;
691
692        // FIXME CMS-2297
693        // Check user right
694        // checkUserRight(object, "Plugin_Explorer_Folder_Add");
695
696        if (!checkLock(parent))
697        {
698            getLogger().warn("User '{}' try to modify collection '{}' but it is locked by another user", _currentUserProvider.getUser(), parent.getName());
699            if (errors != null)
700            {
701                errors.add("locked");
702            }
703            return null;
704        }
705
706        if (!renameIfExists && parent.hasChild(originalName))
707        {
708            getLogger().warn("The object '{}' can not be renamed in '{}' : a object of same name already exists.", parent.getName(), originalName);
709            if (errors != null)
710            {
711                errors.add("already-exist");
712            }
713            return null;
714        }
715
716        int index = 2;
717        String name = originalName;
718        while (parent.hasChild(name))
719        {
720            name = originalName + " (" + index + ")";
721            index++;
722        }
723
724        ResourceCollection child = parent.createChild(name, getResourceCollectionType());
725        parent.saveChanges();
726
727        // Notify listeners
728        Map<String, Object> eventParams = new HashMap<>();
729        eventParams.put(ObservationConstants.ARGS_ID, child.getId());
730        eventParams.put(ObservationConstants.ARGS_PARENT_ID, parent.getId());
731        eventParams.put(ObservationConstants.ARGS_NAME, child.getName());
732        eventParams.put(ObservationConstants.ARGS_PATH, child.getPath());
733
734        _observationManager.notify(new Event(ObservationConstants.EVENT_COLLECTION_CREATED, _currentUserProvider.getUser(), eventParams));
735
736        return child;
737    }
738
739    /**
740     * Get the type of child resource collection
741     * 
742     * @return the type of child resource collection
743     */
744    public String getResourceCollectionType()
745    {
746        return JCRResourcesCollectionFactory.RESOURCESCOLLECTION_NODETYPE;
747    }
748
749    /**
750     * Rename a resource, or resource collection
751     * 
752     * @param id The id of the object to rename
753     * @param name The desired name to set to the object.
754     * @return The result map with id, name and message keys
755     * @throws RepositoryException If there is a repository error
756     */
757    @Callable
758    public Map<String, Object> renameObject(String id, String name) throws RepositoryException
759    {
760        Map<String, Object> result = new HashMap<>();
761
762        JCRAmetysObject object = _resolver.resolveById(id);
763        List<String> errors = new LinkedList<>();
764        JCRAmetysObject newObject = renameObject(object, name, errors);
765
766        if (!errors.isEmpty())
767        {
768            String error = errors.get(0);
769            result.put("message", error);
770        }
771        else
772        {
773            result.put("id", newObject.getId());
774            result.put("name", name);
775        }
776
777        return result;
778    }
779
780    /**
781     * Rename a resource, or resource collection
782     * 
783     * @param object The object to rename
784     * @param name The desired name to set to the object.
785     * @param errors An optional list of possible error messages in case the
786     *            operation failed. Possible values are: locked, already-exist.
787     * @return The new object
788     * @throws RepositoryException If there is a repository error
789     */
790    public JCRAmetysObject renameObject(JCRAmetysObject object, String name, List<String> errors) throws RepositoryException
791    {
792        assert object != null;
793        assert name != null;
794
795        String legalName = Text.escapeIllegalJcrChars(name);
796
797        // Check node is not the root node
798        if ("ametys-internal:resources".equals(object.getName()))
799        {
800            throw new IllegalStateException("The resources root node can not be renamed !");
801        }
802
803        // FIXME CMS-2297
804        // Check user right
805        // checkUserRight (object, object instanceof Resource ?
806        // "Plugin_Explorer_File_Rename" : "Plugin_Explorer_Folder_Edit");
807
808        Node node = object.getNode();
809        String oldName = object.getName();
810        String oldObjectPath = object.getPath();
811
812        if (!checkLock(object))
813        {
814            getLogger().warn("User '{}' is trying to rename object '{}' but it is locked by another user", _currentUserProvider.getUser(), object.getName());
815            if (errors != null)
816            {
817                errors.add("locked");
818            }
819            return null;
820        }
821        else if (node.getParent().hasNode(legalName))
822        {
823            getLogger().warn("The object '{}' cannot be renamed in '{}' : an object with the same name already exists.", object.getName(), name);
824            if (errors != null)
825            {
826                errors.add("already-exist");
827            }
828            return null;
829        }
830        else
831        {
832            String oldResourcePath = object instanceof Resource ? ((Resource) object).getResourcePath() : ((ResourceCollection) object).getExplorerPath();
833
834            node.getSession().move(node.getPath(), node.getParent().getPath() + '/' + legalName);
835            node.getSession().save();
836
837            // Resolve the new ametys object
838            JCRAmetysObject newObject = (JCRAmetysObject) _resolver.resolve(node, false);
839
840            // Notify listeners
841            Map<String, Object> eventParams = new HashMap<>();
842            eventParams.put(ObservationConstants.ARGS_ID, newObject.getId());
843            eventParams.put(ObservationConstants.ARGS_PARENT_ID, newObject.getParent().getId());
844            eventParams.put(ObservationConstants.ARGS_NAME, newObject.getName());
845            eventParams.put(ObservationConstants.ARGS_PATH, newObject.getPath());
846            eventParams.put("object.old.name", oldName);
847            eventParams.put("object.old.path", oldObjectPath);
848
849            if (object instanceof Resource)
850            {
851                eventParams.put(ObservationConstants.ARGS_RESOURCE_PATH, ((Resource) object).getResourcePath());
852                eventParams.put("resource.old.path", oldResourcePath);
853            }
854            else
855            {
856                eventParams.put(ObservationConstants.ARGS_EXPLORER_PATH, ((ResourceCollection) object).getExplorerPath());
857                eventParams.put("explorer.old.path", oldResourcePath);
858            }
859
860            _observationManager.notify(new Event(object instanceof JCRResource ? ObservationConstants.EVENT_RESOURCE_RENAMED : ObservationConstants.EVENT_COLLECTION_RENAMED,
861                    _currentUserProvider.getUser(), eventParams));
862
863            return newObject;
864        }
865    }
866
867    /**
868     * Delete an object
869     * 
870     * @param ids The list of identifiers for the objects to delete
871     * @return The result map with a message key in case of an error
872     */
873    @Callable
874    public Map<String, Object> deleteObject(List<String> ids)
875    {
876        assert ids != null;
877
878        Map<String, Object> result = new HashMap<>();
879
880        for (String id : ids)
881        {
882            RemovableAmetysObject object = (RemovableAmetysObject) _resolver.resolveById(id);
883            List<String> errors = new LinkedList<>();
884            deleteObject(object, errors);
885
886            if (!errors.isEmpty())
887            {
888                String error = errors.get(0);
889                result.put("message", error);
890                return result;
891            }
892        }
893
894        return result;
895    }
896
897    /**
898     * Delete an object
899     * 
900     * @param object The object to delete
901     * @param errors An optional list of possible error messages in case the
902     *            creation failed. Possible values are: locked.
903     * @return the parent id of the removed object
904     */
905    public String deleteObject(RemovableAmetysObject object, List<String> errors)
906    {
907        // Check node is not the root node
908        if ("ametys-internal:resources".equals(object.getName()))
909        {
910            throw new IllegalStateException("The resources root node can not be deleted !");
911        }
912
913        // Check user right
914        // FIXME CMS-2297
915        // checkUserRight(object, object instanceof Resource ?
916        // "Plugin_Explorer_File_Delete" : "Plugin_Explorer_Folder_Delete");
917
918        if (!checkLock(object))
919        {
920            getLogger().warn("User '{}' is trying to delete object '{}' but it is locked by another user", _currentUserProvider.getUser(), object.getName());
921            if (errors != null)
922            {
923                errors.add("locked");
924            }
925            return null;
926        }
927
928        ModifiableResourceCollection parent = object.getParent();
929        String eventType = object instanceof Resource ? ObservationConstants.EVENT_RESOURCE_DELETED : ObservationConstants.EVENT_COLLECTION_DELETED;
930        String parentId = parent.getId();
931
932        Map<String, Object> eventParams = new HashMap<>();
933        eventParams.put(ObservationConstants.ARGS_PARENT_ID, parentId);
934        eventParams.put(ObservationConstants.ARGS_ID, object.getId());
935        eventParams.put(ObservationConstants.ARGS_NAME, object.getName());
936        eventParams.put(ObservationConstants.ARGS_PATH, object.getPath());
937
938        if (object instanceof Resource)
939        {
940            eventParams.put(ObservationConstants.ARGS_RESOURCE_PATH, ((Resource) object).getResourcePath());
941        }
942        else
943        {
944            eventParams.put(ObservationConstants.ARGS_EXPLORER_PATH, ((ResourceCollection) object).getExplorerPath());
945        }
946
947        object.remove();
948        parent.saveChanges();
949
950        _observationManager.notify(new Event(eventType, _currentUserProvider.getUser(), eventParams));
951
952        return parentId;
953    }
954
955    /**
956     * Rename a resource
957     * 
958     * @param id The id of the resource to rename
959     * @param name The desired name to set to the resource.
960     * @return The result map with id, name and message keys
961     * @throws RepositoryException If there is a repository error
962     */
963    @Callable
964    public Map<String, Object> renameResource(String id, String name) throws RepositoryException
965    {
966        Map<String, Object> result = new HashMap<>();
967
968        JCRResource resource = _resolver.resolveById(id);
969        List<String> errors = new LinkedList<>();
970
971        Resource newResource = renameResource(resource, name, errors);
972
973        if (!errors.isEmpty())
974        {
975            String error = errors.get(0);
976            result.put("message", error);
977        }
978        else
979        {
980            result.put("id", newResource.getId());
981            result.put("name", name);
982        }
983
984        return result;
985    }
986
987    /**
988     * Rename a resource
989     * 
990     * @param resource The resource to rename
991     * @param name The desired name to set to the resource.
992     * @param errors An optional list of possible error messages in case the
993     *            operation failed. Possible values are: locked, already-exist.
994     * @return The new resource
995     * @throws RepositoryException If there is a repository error
996     */
997    public JCRResource renameResource(JCRResource resource, String name, List<String> errors) throws RepositoryException
998    {
999        assert resource != null;
1000        assert name != null;
1001
1002        String legalName = Text.escapeIllegalJcrChars(name);
1003
1004        // FIXME API getNode should use the new rename method
1005        String oldName = resource.getName();
1006        String oldPath = resource.getPath();
1007        String oldResourcePath = resource.getResourcePath();
1008
1009        Node node = resource.getNode();
1010
1011        // FIXME CMS-2297 Check user right
1012        // checkUserRight (object, "Plugin_Explorer_File_Rename");
1013
1014        // Check lock on resource
1015        if (!checkLock(resource))
1016        {
1017            getLogger().warn("User '{}' is trying to rename resource '{}' but it is locked by another user", _currentUserProvider.getUser(), resource.getName());
1018            if (errors != null)
1019            {
1020                errors.add("locked");
1021            }
1022            return null;
1023        }
1024
1025        if (node.getParent().hasNode(legalName))
1026        {
1027            getLogger().warn("The resource '{}' cannot be renamed in '{}' : an object with the same name already exists.", resource.getName(), name);
1028            if (errors != null)
1029            {
1030                errors.add("already-exist");
1031            }
1032            return null;
1033        }
1034
1035        String mimeType = _cocoonContext.getMimeType(legalName.toLowerCase());
1036        mimeType = mimeType == null ? "application/unknown" : mimeType;
1037        resource.setMimeType(mimeType);
1038
1039        node.getSession().move(node.getPath(), node.getParent().getPath() + '/' + legalName);
1040
1041        node.getSession().save();
1042
1043        // Resolve the new ametys object
1044        JCRResource newObject = (JCRResource) _resolver.resolve(node, false);
1045
1046        // Notify listeners
1047        Map<String, Object> eventParams = new HashMap<>();
1048
1049        eventParams.put(ObservationConstants.ARGS_ID, newObject.getId());
1050        eventParams.put(ObservationConstants.ARGS_PARENT_ID, newObject.getParent().getId());
1051        eventParams.put(ObservationConstants.ARGS_NAME, newObject.getName());
1052        eventParams.put(ObservationConstants.ARGS_PATH, newObject.getPath());
1053        eventParams.put("object.old.name", oldName);
1054        eventParams.put("object.old.path", oldPath);
1055
1056        eventParams.put(ObservationConstants.ARGS_RESOURCE_PATH, newObject.getResourcePath());
1057        eventParams.put("resource.old.path", oldResourcePath);
1058
1059        _observationManager.notify(new Event(ObservationConstants.EVENT_RESOURCE_RENAMED, _currentUserProvider.getUser(), eventParams));
1060
1061        return newObject;
1062    }
1063
1064    /**
1065     * Copy file resources
1066     * 
1067     * @param ids The list of identifiers for the resources to copy
1068     * @param target The id of target to copy into
1069     * @return The result map with a message key in case of an error or with the
1070     *         list of uncopied/copied resources
1071     * @throws RepositoryException If there is a repository error
1072     */
1073    @Callable
1074    public Map<String, Object> copyResource(List<String> ids, String target) throws RepositoryException
1075    {
1076        assert ids != null;
1077        assert target != null;
1078
1079        AmetysObject object = _resolver.resolveById(target);
1080        if (!(object instanceof ModifiableResourceCollection))
1081        {
1082            throw new IllegalClassException(ModifiableResourceCollection.class, object.getClass());
1083        }
1084
1085        return copyResource(ids, (ModifiableResourceCollection) object);
1086    }
1087
1088    /**
1089     * Copy file resources
1090     * 
1091     * @param ids The list of identifiers for the resources to copy
1092     * @param target The target to copy into
1093     * @return The result map with a message key in case of an error or with the
1094     *         list of uncopied/copied resources
1095     * @throws RepositoryException If there is a repository error
1096     */
1097    public Map<String, Object> copyResource(List<String> ids, ModifiableResourceCollection target) throws RepositoryException
1098    {
1099        Map<String, Object> result = new HashMap<>();
1100        List<String> uncopiedResources = new ArrayList<>();
1101        List<String> copiedResourceIds = new ArrayList<>();
1102
1103        assert ids != null;
1104        assert target != null;
1105
1106        if (!checkLock(target))
1107        {
1108            getLogger().warn("User '{}' try to copy objet to '{}' but it is locked by another user", _currentUserProvider.getUser(), target.getName());
1109            result.put("message", "locked");
1110            return result;
1111        }
1112
1113        // FIXME CMS-2297
1114        // checkUserRight(object, "Plugin_Explorer_File_Add");
1115
1116        for (String id : ids)
1117        {
1118            Resource resourceToCopy = _resolver.resolveById(id);
1119            String fileName = resourceToCopy.getName();
1120
1121            if (target.hasChild(fileName))
1122            {
1123                getLogger().warn("The resource '" + fileName + "' can not be copied : an object of same name already exists in the target collection");
1124                result.put("message", "already-exist");
1125                uncopiedResources.add(fileName);
1126            }
1127            else
1128            {
1129                ModifiableResource resource = createResource(target, fileName);
1130
1131                try (InputStream is = resourceToCopy.getInputStream())
1132                {
1133                    updateResource(resource, is, fileName);
1134                }
1135                catch (IOException e)
1136                {
1137                    getLogger().warn("An error occurred while closing the ressource " + resource.getId(), e);
1138                }
1139
1140                copiedResourceIds.add(resource.getId());
1141            }
1142        }
1143
1144        target.saveChanges();
1145
1146        for (String id : copiedResourceIds)
1147        {
1148            ModifiableResource resource = _resolver.resolveById(id);
1149
1150            // Create version
1151            checkpoint(resource);
1152
1153            // Notify listeners
1154            Map<String, Object> eventParams = new HashMap<>();
1155            Map<String, Resource> addedResource = new HashMap<>();
1156            addedResource.put(resource.getId(), resource);
1157            eventParams.put(ObservationConstants.ARGS_RESOURCES, addedResource);
1158            eventParams.put(ObservationConstants.ARGS_PARENT_ID, target.getId());
1159            eventParams.put(ObservationConstants.ARGS_PARENT_PATH, target.getPath());
1160
1161            _observationManager.notify(new Event(ObservationConstants.EVENT_RESOURCE_CREATED, _currentUserProvider.getUser(), eventParams));
1162        }
1163
1164        if (uncopiedResources.size() > 0)
1165        {
1166            result.put("uncopied-resources", uncopiedResources);
1167        }
1168
1169        if (copiedResourceIds.size() > 0)
1170        {
1171            result.put("copied-resources", copiedResourceIds);
1172        }
1173
1174        return result;
1175    }
1176
1177    /**
1178     * Move objects
1179     * 
1180     * @param ids The list of identifiers for the objects to move
1181     * @param targetId The id of target to move into
1182     * @return The result map with a message key in case of an error or with the
1183     *         list of unmoved/moved objects
1184     * @throws RepositoryException If there is a repository error
1185     */
1186    @Callable
1187    public Map<String, Object> moveObject(List<String> ids, String targetId) throws RepositoryException
1188    {
1189        assert ids != null;
1190        assert targetId != null;
1191
1192        AmetysObject target = _resolver.resolveById(targetId);
1193        if (!(target instanceof JCRTraversableAmetysObject))
1194        {
1195            throw new IllegalClassException(JCRTraversableAmetysObject.class, target.getClass());
1196        }
1197
1198        return moveObject(ids, (JCRTraversableAmetysObject) target);
1199    }
1200
1201    /**
1202     * Move objects
1203     * 
1204     * @param ids The list of identifiers for the objects to move
1205     * @param targetNode The target to move into
1206     * @return The result map with a message key in case of an error or with the
1207     *         list of unmoved/moved objects
1208     * @throws RepositoryException If there is a repository error
1209     */
1210    public Map<String, Object> moveObject(List<String> ids, JCRTraversableAmetysObject targetNode) throws RepositoryException
1211    {
1212        Map<String, Object> result = new HashMap<>();
1213        List<String> unmovedObjects = new ArrayList<>();
1214        List<String> movedObjectIds = new ArrayList<>();
1215
1216        assert ids != null;
1217        assert targetNode != null;
1218
1219        if (!checkLock(targetNode))
1220        {
1221            getLogger().warn("User '" + _currentUserProvider.getUser() + "' try to move objet to '" + targetNode.getName() + "' but it is locked by another user");
1222            result.put("message", "locked");
1223            return result;
1224        }
1225
1226        for (String id : ids)
1227        {
1228            JCRAmetysObject object = (JCRAmetysObject) _resolver.resolveById(id);
1229
1230            if (targetNode.hasChild(object.getName()))
1231            {
1232                getLogger().warn("The object '" + object.getName() + "' can not be moved : a object of same name already exists in the target collection");
1233                result.put("message", "already-exist");
1234                unmovedObjects.add(object.getName());
1235            }
1236            else
1237            {
1238                String oldResourcePath = object instanceof Resource ? ((Resource) object).getResourcePath() : ((ExplorerNode) object).getExplorerPath();
1239
1240                Map<String, Object> eventParams = new HashMap<>();
1241                eventParams.put("object.old.path", object.getPath());
1242
1243                Session session = object.getNode().getSession();
1244                session.move(object.getNode().getPath(), targetNode.getNode().getPath() + "/" + object.getNode().getName());
1245
1246                if (object instanceof Resource)
1247                {
1248                    eventParams.put(ObservationConstants.ARGS_RESOURCE_PATH, ((Resource) object).getResourcePath());
1249                    eventParams.put("resource.old.path", oldResourcePath);
1250                }
1251                else
1252                {
1253                    eventParams.put("explorer.old.path", oldResourcePath);
1254                }
1255
1256                eventParams.put(ObservationConstants.ARGS_PATH, ((ExplorerNode) targetNode).getExplorerPath() + "/" + object.getName());
1257                eventParams.put(ObservationConstants.ARGS_PARENT_ID, ((ExplorerNode) targetNode).getId());
1258                eventParams.put(ObservationConstants.ARGS_ID, id);
1259                eventParams.put(ObservationConstants.ARGS_NAME, object.getName());
1260
1261                session.save();
1262                movedObjectIds.add(object.getId());
1263
1264                if (object instanceof Resource)
1265                {
1266                    eventParams.put(ObservationConstants.ARGS_RESOURCE_PATH, ((Resource) object).getResourcePath());
1267                    eventParams.put("resource.old.path", oldResourcePath);
1268                    _observationManager.notify(new Event(ObservationConstants.EVENT_RESOURCE_MOVED, _currentUserProvider.getUser(), eventParams));
1269                }
1270                else if (object instanceof ResourceCollection)
1271                {
1272                    _observationManager.notify(new Event(ObservationConstants.EVENT_COLLECTION_MOVED, _currentUserProvider.getUser(), eventParams));
1273                }
1274                else if (object instanceof Calendar)
1275                {
1276                    _observationManager.notify(new Event(ObservationConstants.EVENT_CALENDAR_MOVED, _currentUserProvider.getUser(), eventParams));
1277                }
1278                else if (object instanceof JCRThread)
1279                {
1280                    _observationManager.notify(new Event(ObservationConstants.EVENT_THREAD_MOVED, _currentUserProvider.getUser(), eventParams));
1281                }
1282                else
1283                {
1284                    getLogger().warn("Object " + object.getId() + " of class '" + object.getClass().getName() + "' was moved. This type is unknown.");
1285                }
1286            }
1287        }
1288
1289        if (!unmovedObjects.isEmpty())
1290        {
1291            result.put("unmoved-objects", unmovedObjects);
1292        }
1293
1294        if (!movedObjectIds.isEmpty())
1295        {
1296            result.put("moved-objects", movedObjectIds);
1297        }
1298
1299        return result;
1300    }
1301
1302    /**
1303     * Get the history versions of a resource
1304     * 
1305     * @param id the id of resource
1306     * @return The versions
1307     * @throws RepositoryException if an error occurred
1308     */
1309    @Callable
1310    public List<Map<String, Object>> resourceHistory(String id) throws RepositoryException
1311    {
1312        JCRResource resource = _resolver.resolveById(id);
1313
1314        List<Map<String, Object>> versions = new ArrayList<>();
1315
1316        List<VersionInformation> versionsInfo = new ArrayList<>();
1317
1318        for (String revision : resource.getAllRevisions())
1319        {
1320            VersionInformation versionInformation = new VersionInformation(revision, resource.getRevisionTimestamp(revision));
1321
1322            for (String label : resource.getLabels(revision))
1323            {
1324                versionInformation.addLabel(label);
1325            }
1326
1327            versionsInfo.add(versionInformation);
1328        }
1329
1330        // Sort by date descendant
1331        Collections.sort(versionsInfo, new Comparator<VersionInformation>()
1332        {
1333            public int compare(VersionInformation o1, VersionInformation o2)
1334            {
1335                try
1336                {
1337                    return -o1.getCreatedAt().compareTo(o2.getCreatedAt());
1338                }
1339                catch (RepositoryException e)
1340                {
1341                    throw new RuntimeException("Unable to retrieve a creation date", e);
1342                }
1343            }
1344        });
1345
1346        for (VersionInformation versionInformation : versionsInfo)
1347        {
1348            Map<String, Object> version = new HashMap<>();
1349
1350            for (String label : versionInformation.getLabels())
1351            {
1352                version.put(label, label);
1353            }
1354
1355            version.put("rawName", versionInformation.getVersionRawName());
1356            version.put("name", versionInformation.getVersionName());
1357            version.put("createdAt", DateUtils.dateToString(versionInformation.getCreatedAt()));
1358
1359            try
1360            {
1361                resource.switchToRevision(versionInformation.getVersionRawName());
1362                UserIdentity author = resource.getLastContributor();
1363                version.put("author", _userHelper.user2json(author));
1364            }
1365            catch (AmetysRepositoryException e)
1366            {
1367                // Do nothing. Can append with old version history (before 4.0)
1368            }
1369
1370            versions.add(version);
1371        }
1372
1373        return versions;
1374    }
1375
1376    /**
1377     * This action restores an old version of a {@link Resource}.
1378     * 
1379     * @param id the id of resource
1380     * @param versionName the name of version to restore
1381     */
1382    @Callable
1383    public void restoreResource(String id, String versionName)
1384    {
1385        JCRResource resource = _resolver.resolveById(id);
1386        resource.restoreFromRevision(versionName);
1387
1388        resource.setLastContributor(_currentUserProvider.getUser());
1389        resource.setLastModified(new Date());
1390        resource.saveChanges();
1391        resource.checkpoint();
1392    }
1393
1394    /**
1395     * Determines if a resource with given name already exists
1396     * 
1397     * @param parentId the id of parent collection
1398     * @param name the name of resource
1399     * @return true if a resource with same name exists
1400     */
1401    @Callable
1402    public boolean resourceExists(String parentId, String name)
1403    {
1404        return resourceExists((TraversableAmetysObject) _resolver.resolveById(parentId), name);
1405    }
1406
1407    /**
1408     * Determines if a resource with given name already exists
1409     * 
1410     * @param parent the parent collection
1411     * @param name the name of resource
1412     * @return true if a resource with same name exists
1413     */
1414    public boolean resourceExists(TraversableAmetysObject parent, String name)
1415    {
1416        return parent.hasChild(name);
1417    }
1418
1419    /**
1420     * Set the Dublin Core metadata of a {@link Resource}.
1421     * 
1422     * @param resourceId the id of resource
1423     * @param values the DC values
1424     * @throws ProcessingException if an error occurred
1425     */
1426    @Callable
1427    public void setDCMetadata(String resourceId, Map<String, Object> values) throws ProcessingException
1428    {
1429        setDCMetadata((ModifiableResource) _resolver.resolveById(resourceId), values);
1430    }
1431
1432    /**
1433     * Set the Dublin Core metadata of a {@link Resource}.
1434     * 
1435     * @param resource the resource
1436     * @param values the DC values
1437     */
1438    public void setDCMetadata(ModifiableResource resource, Map<String, Object> values)
1439    {
1440        String title = (String) values.get("dc_title");
1441        String creator = (String) values.get("dc_creator");
1442        Object subject = values.get("dc_subject");
1443        @SuppressWarnings("unchecked")
1444        List<String> subjects = subject instanceof List ? (List<String>) subject : Collections.emptyList();
1445        String description = (String) values.get("dc_description");
1446        String publisher = (String) values.get("dc_publisher");
1447        String contributor = (String) values.get("dc_contributor");
1448        String dateStr = (String) values.get("dc_date");
1449        String type = (String) values.get("dc_type");
1450        String source = (String) values.get("dc_source");
1451        String language = (String) values.get("dc_language");
1452        String relation = (String) values.get("dc_relation");
1453        String coverage = (String) values.get("dc_coverage");
1454        String rights = (String) values.get("dc_rights");
1455
1456        try
1457        {
1458            // FIXME CMS-2297 Check user right
1459            // checkUserRight (object, "Plugin_Explorer_File_Edit_DC_Metadata");
1460
1461            _updateTitleIfNeeded(resource, title);
1462            _updateCreatorIfNeeded(resource, creator);
1463            _updateDateIfNeeded(resource, dateStr);
1464            _updateDCSubjectIfNeeded(resource, subjects);
1465            _updateDescriptionIfNeeded(resource, description);
1466            _updatePublisherIfNeeded(resource, publisher);
1467            _updateContributorIfNeeded(resource, contributor);
1468            _updateTypeIfNeeded(resource, type);
1469            _updateSourceIfNeeded(resource, source);
1470            _updateLanguageIfNeeded(resource, language);
1471            _updateRelationIfNeeded(resource, relation);
1472            _updateCoverageIfNeeded(resource, coverage);
1473            _updateRightsIfNeeded(resource, rights);
1474
1475            _saveChangesIfNeeded(resource);
1476
1477        }
1478        catch (AmetysRepositoryException e)
1479        {
1480            String errorMsg = String.format("Exception while trying to set Dublin Core metadata on resource %s", resource.getId());
1481            getLogger().error(errorMsg, e);
1482            throw new AmetysRepositoryException(errorMsg, e);
1483        }
1484    }
1485
1486    private void _updateTitleIfNeeded(ModifiableResource resource, String title)
1487    {
1488        if (!Objects.equals(title, resource.getDCTitle()))
1489        {
1490            resource.setDCTitle(title);
1491        }
1492    }
1493
1494    private void _updateCreatorIfNeeded(ModifiableResource resource, String creator)
1495    {
1496        if (!Objects.equals(creator, resource.getDCCreator()))
1497        {
1498            resource.setDCCreator(creator);
1499        }
1500    }
1501
1502    private void _updateDateIfNeeded(ModifiableResource resource, String dateStr)
1503    {
1504        Date date = null;
1505        if (StringUtils.isNotEmpty(dateStr))
1506        {
1507            LocalDateTime ldt = LocalDateTime.parse(dateStr, DateTimeFormatter.ISO_DATE_TIME);
1508            date = DateUtils.asDate(ldt);
1509        }
1510
1511        if (!Objects.equals(date, resource.getDCDate()))
1512        {
1513            resource.setDCDate(date);
1514        }
1515    }
1516
1517    private void _updateRightsIfNeeded(ModifiableResource resource, String rights)
1518    {
1519        if (!Objects.equals(rights, resource.getDCRights()))
1520        {
1521            resource.setDCRights(rights);
1522        }
1523    }
1524
1525    private void _updateCoverageIfNeeded(ModifiableResource resource, String coverage)
1526    {
1527        if (!Objects.equals(coverage, resource.getDCCoverage()))
1528        {
1529            resource.setDCCoverage(coverage);
1530        }
1531    }
1532
1533    private void _updateRelationIfNeeded(ModifiableResource resource, String relation)
1534    {
1535        if (!Objects.equals(relation, resource.getDCRelation()))
1536        {
1537            resource.setDCRelation(relation);
1538        }
1539    }
1540
1541    private void _updateLanguageIfNeeded(ModifiableResource resource, String language)
1542    {
1543        if (!Objects.equals(language, resource.getDCLanguage()))
1544        {
1545            resource.setDCLanguage(language);
1546        }
1547    }
1548
1549    private void _updateSourceIfNeeded(ModifiableResource resource, String source)
1550    {
1551        if (!Objects.equals(source, resource.getDCSource()))
1552        {
1553            resource.setDCSource(source);
1554        }
1555    }
1556
1557    private void _updateTypeIfNeeded(ModifiableResource resource, String type)
1558    {
1559        if (!Objects.equals(type, resource.getDCType()))
1560        {
1561            resource.setDCType(type);
1562        }
1563    }
1564
1565    private void _updateContributorIfNeeded(ModifiableResource resource, String contributor)
1566    {
1567        if (!Objects.equals(contributor, resource.getDCContributor()))
1568        {
1569            resource.setDCContributor(contributor);
1570        }
1571    }
1572
1573    private void _updatePublisherIfNeeded(ModifiableResource resource, String publisher)
1574    {
1575        if (!Objects.equals(publisher, resource.getDCPublisher()))
1576        {
1577            resource.setDCPublisher(publisher);
1578        }
1579    }
1580
1581    private void _updateDescriptionIfNeeded(ModifiableResource resource, String description)
1582    {
1583        if (!Objects.equals(description, resource.getDCDescription()))
1584        {
1585            resource.setDCDescription(description);
1586        }
1587    }
1588
1589    private void _updateDCSubjectIfNeeded(ModifiableResource resource, List<String> subjects)
1590    {
1591        String[] trimSubject = subjects.stream().map(StringUtils::trim).toArray(String[]::new);
1592        if (!Objects.deepEquals(trimSubject, resource.getDCSubject()))
1593        {
1594            resource.setDCSubject(trimSubject);
1595        }
1596    }
1597    
1598    private void _saveChangesIfNeeded(ModifiableResource resource)
1599    {
1600        if (resource.needsSave())
1601        {
1602            resource.setLastContributor(_currentUserProvider.getUser());
1603            resource.setLastModified(new Date());
1604
1605            ModifiableResourceCollection parent = resource.getParent();
1606            parent.saveChanges();
1607
1608            if (resource instanceof VersionableAmetysObject)
1609            {
1610                // Create first version
1611                ((VersionableAmetysObject) resource).checkpoint();
1612            }
1613
1614            // Notify listeners of resource update.
1615            Map<String, Object> eventParams = new HashMap<>();
1616            eventParams.put(ObservationConstants.ARGS_ID, resource.getId());
1617            eventParams.put(ObservationConstants.ARGS_PARENT_ID, parent.getId());
1618            eventParams.put(ObservationConstants.ARGS_NAME, resource.getName());
1619            eventParams.put(ObservationConstants.ARGS_PATH, resource.getPath());
1620
1621            _observationManager.notify(new Event(ObservationConstants.EVENT_RESOURCE_UPDATED, _currentUserProvider.getUser(), eventParams));
1622        }
1623    }
1624
1625    /**
1626     * Get the DublinCore metadata of a {@link DublinCoreAwareAmetysObject}
1627     * 
1628     * @param id the id of resource
1629     * @return the DC metadata
1630     */
1631    @Callable
1632    public Map<String, Object> getDCMetadata(String id)
1633    {
1634        AmetysObject object = _resolver.resolveById(id);
1635        if (object instanceof DublinCoreAwareAmetysObject)
1636        {
1637            DublinCoreAwareAmetysObject dcObject = (DublinCoreAwareAmetysObject) object;
1638
1639            Map<String, Object> metadata = new HashMap<>();
1640
1641            metadata.put("id", id);
1642
1643            Map<String, Object> values = new HashMap<>();
1644
1645            values.put("dc_title", dcObject.getDCTitle());
1646            values.put("dc_creator", dcObject.getDCCreator());
1647            values.put("dc_subject", dcObject.getDCSubject());
1648            values.put("dc_description", dcObject.getDCDescription());
1649            values.put("dc_type", dcObject.getDCType());
1650            values.put("dc_publisher", dcObject.getDCPublisher());
1651            values.put("dc_contributor", dcObject.getDCContributor());
1652            values.put("dc_date", DateUtils.dateToString(dcObject.getDCDate()));
1653            values.put("dc_format", dcObject.getDCFormat());
1654            values.put("dc_identifier", dcObject.getDCIdentifier());
1655            values.put("dc_source", dcObject.getDCSource());
1656            values.put("dc_language", dcObject.getDCLanguage());
1657            values.put("dc_relation", dcObject.getDCRelation());
1658            values.put("dc_coverage", dcObject.getDCCoverage());
1659            values.put("dc_rights", dcObject.getDCRights());
1660
1661            metadata.put("values", values);
1662            return metadata;
1663        }
1664        else
1665        {
1666            throw new IllegalArgumentException("Object of id " + id + " is not Dublin Core aware.");
1667        }
1668    }
1669
1670    /**
1671     * Creates a new CMIS folder (see {@link CMISRootResourcesCollection})
1672     * 
1673     * @param parentId the id of parent folder
1674     * @param originalName the original name if CMIS folder
1675     * @param url The url of CMIS repository
1676     * @param login The user's login to access CMIS repository
1677     * @param password The user's password to access CMIS repository
1678     * @param repoId The id of CMIS repository
1679     * @param renameIfExists true to automatically renamed CMIS folder if
1680     *            requested name already exists
1681     * @return the result map with id of created node
1682     */
1683    @Callable
1684    public Map<String, Object> addCMISCollection(String parentId, String originalName, String url, String login, String password, String repoId, boolean renameIfExists)
1685    {
1686        Map<String, Object> result = new HashMap<>();
1687
1688        AmetysObject object = _resolver.resolveById(parentId);
1689        if (!(object instanceof ModifiableResourceCollection))
1690        {
1691            throw new IllegalClassException(ModifiableResourceCollection.class, object.getClass());
1692        }
1693
1694        ModifiableResourceCollection collection = (ModifiableResourceCollection) object;
1695
1696        // FIXME CMS-2297
1697        // checkUserRight(object, "Plugin_Explorer_CMIS_Add");
1698
1699        if (!checkLock(collection))
1700        {
1701            getLogger().warn("User '" + _currentUserProvider.getUser() + "' try to modify collection '" + object.getName() + "' but it is locked by another user");
1702            result.put("message", "locked");
1703            return result;
1704        }
1705
1706        if (!renameIfExists && collection.hasChild(originalName))
1707        {
1708            getLogger().warn("The object '" + object.getName() + "' can not be renamed in '" + originalName + "' : a object of same name already exists.");
1709            result.put("message", "already-exist");
1710            return result;
1711        }
1712
1713        int index = 1;
1714        String name = originalName;
1715        while (collection.hasChild(name))
1716        {
1717            name = originalName + " (" + index + ")";
1718            index++;
1719        }
1720
1721        CMISRootResourcesCollection child = collection.createChild(name, CMISTreeFactory.CMIS_ROOT_COLLECTION_NODETYPE);
1722
1723        child.setRepositoryUrl(url);
1724        child.setRepositoryId(repoId);
1725        child.setUser(login);
1726        child.setPassword(password);
1727
1728        collection.saveChanges();
1729
1730        result.put("id", child.getId());
1731        result.put("parentID", parentId);
1732        result.put("name", name);
1733
1734        // Notify listeners
1735        Map<String, Object> eventParams = new HashMap<>();
1736        eventParams.put(ObservationConstants.ARGS_ID, child.getId());
1737        eventParams.put(ObservationConstants.ARGS_PARENT_ID, object.getId());
1738        eventParams.put(ObservationConstants.ARGS_NAME, child.getName());
1739        eventParams.put(ObservationConstants.ARGS_PATH, child.getPath());
1740
1741        _observationManager.notify(new Event(ObservationConstants.EVENT_COLLECTION_CREATED, _currentUserProvider.getUser(), eventParams));
1742
1743        return result;
1744    }
1745
1746    /**
1747     * Edits a CMIS folder (see {@link CMISRootResourcesCollection})
1748     * 
1749     * @param id the id of CMIS folder
1750     * @param url The url of CMIS repository
1751     * @param login The user's login to access CMIS repository
1752     * @param password The user's password to access CMIS repository
1753     * @param repoId The id of CMIS repository
1754     * @return the result map with id of edited node
1755     */
1756    @Callable
1757    public Map<String, Object> editCMISCollection(String id, String url, String login, String password, String repoId)
1758    {
1759        Map<String, Object> result = new HashMap<>();
1760
1761        CMISRootResourcesCollection object = _resolver.resolveById(id);
1762
1763        // FIXME CMS-2297
1764        // checkUserRight(object, "Plugin_Explorer_CMIS_Add");
1765
1766        object.setRepositoryUrl(url);
1767        object.setRepositoryId(repoId);
1768        object.setUser(login);
1769        object.setPassword(password);
1770
1771        object.saveChanges();
1772
1773        // Notify listeners
1774        Map<String, Object> eventParams = new HashMap<>();
1775        eventParams.put(ObservationConstants.ARGS_ID, object.getId());
1776
1777        _observationManager.notify(new Event(ObservationConstants.EVENT_CMIS_COLLECTION_UPDATED, _currentUserProvider.getUser(), eventParams));
1778
1779        result.put("id", object.getId());
1780
1781        return result;
1782    }
1783
1784    /**
1785     * Determines if a object is a {@link CMISRootResourcesCollection}
1786     * 
1787     * @param id The id of object
1788     * @return true if it is a CMIS root folder
1789     */
1790    @Callable
1791    public boolean isCMISCollection(String id)
1792    {
1793        ResourceCollection collection = _resolver.resolveById(id);
1794        return collection instanceof CMISRootResourcesCollection;
1795    }
1796
1797    /**
1798     * Get the CMIS properties of collection
1799     * 
1800     * @param id The id of CMIS collection
1801     * @return the CMIS properties to access CMIS repository
1802     */
1803    @Callable
1804    public Map<String, String> getCMISProperties(String id)
1805    {
1806        Map<String, String> properties = new HashMap<>();
1807
1808        CMISRootResourcesCollection cmis = _resolver.resolveById(id);
1809
1810        properties.put("id", cmis.getId());
1811        properties.put("name", cmis.getName());
1812        properties.put("url", cmis.getRepositoryUrl());
1813        properties.put("login", cmis.getUser());
1814        properties.put("password", cmis.getPassword());
1815        properties.put("repoId", cmis.getRepositoryId());
1816
1817        return properties;
1818    }
1819
1820    /**
1821     * Updates resource input stream and metadata
1822     * 
1823     * @param resource The resource
1824     * @param is The resource input stream
1825     * @param fileName The file name
1826     */
1827    public void updateResource(ModifiableResource resource, InputStream is, String fileName)
1828    {
1829        UserIdentity author = _currentUserProvider.getUser();
1830
1831        String mimeType = _cocoonContext.getMimeType(fileName.toLowerCase());
1832        mimeType = mimeType == null ? "application/unknown" : mimeType;
1833
1834        resource.setData(is, mimeType, new Date(), author);
1835        resource.setLastModified(new Date());
1836
1837        extractResourceMetadata(resource, mimeType);
1838    }
1839
1840    /**
1841     * Extract the resource's metadata and populate the object accordingly.
1842     * 
1843     * @param resource the resource to populate.
1844     * @param mimeType the resource MIME type.
1845     */
1846    public void extractResourceMetadata(ModifiableResource resource, String mimeType)
1847    {
1848        try (InputStream is = resource.getInputStream())
1849        {
1850            Metadata metadata = new Metadata();
1851
1852            try (Reader reader = _tikaProvider.getTika().parse(is, metadata))
1853            {
1854                IOUtils.copy(reader, NullOutputStream.NULL_OUTPUT_STREAM, StandardCharsets.UTF_8);
1855
1856                Collection<ResourceMetadataPopulator> populators = _metadataPopulatorEP.getPopulators(mimeType);
1857                for (ResourceMetadataPopulator populator : populators)
1858                {
1859                    populator.populate(resource, metadata);
1860                }
1861            }
1862        }
1863        catch (Exception e)
1864        {
1865            getLogger().error("Error populating the metadata of resource " + resource.getId(), e);
1866        }
1867    }
1868
1869    /**
1870     * Creates a new version
1871     * 
1872     * @param resource the resource
1873     */
1874    public void checkpoint(ModifiableResource resource)
1875    {
1876        if (resource instanceof VersionableAmetysObject)
1877        {
1878            // Create first version
1879            ((VersionableAmetysObject) resource).checkpoint();
1880        }
1881    }
1882
1883    /**
1884     * Creates a {@link ModifiableResource} under current
1885     * {@link ModifiableResourceCollection}.
1886     * 
1887     * @param collection the parent {@link ModifiableResourceCollection}
1888     * @param name the name of the child resource
1889     * @return the new resource created.
1890     * @throws AmetysRepositoryException if an error occurs.
1891     * @throws RepositoryIntegrityViolationException if an object with the same
1892     *             name already exists and same name siblings is not allowed.
1893     */
1894    public ModifiableResource createResource(ModifiableResourceCollection collection, String name)
1895    {
1896        return collection.createChild(name, collection.getResourceType());
1897    }
1898
1899    // -------------------------------- COMMENTS -------------------------------
1900    /**
1901     * Add a comment to a resource
1902     * 
1903     * @param resourceId The id of the resource
1904     * @param comment The comment
1905     * @return the result
1906     */
1907    @Callable
1908    public Map<String, Object> addComment(String resourceId, String comment)
1909    {
1910        Map<String, Object> result = new HashMap<>();
1911
1912        AmetysObject ao = _resolver.resolveById(resourceId);
1913        UserIdentity currentUser = _currentUserProvider.getUser();
1914
1915        if (!(ao instanceof CommentableResource))
1916        {
1917            throw new IllegalClassException(CommentableResource.class, ao.getClass());
1918        }
1919
1920        if (!checkLock(ao))
1921        {
1922            getLogger().warn("User '{}' is trying to add a comment on resource '{}' but it is locked by another user", currentUser, resourceId);
1923            result.put("message", "locked");
1924            return result;
1925        }
1926
1927        // Check comment rights
1928        CommentableResource resource = (CommentableResource) ao;
1929
1930        if (!_canAddComment(currentUser, resource))
1931        {
1932            getLogger().warn("User '{}' is trying to add a comment on resource '{}' without the convient right.", currentUser, resourceId);
1933            result.put("message", "rights");
1934            return result;
1935        }
1936
1937        org.ametys.plugins.explorer.threads.Thread comments = resource.getComments(true);
1938
1939        if (!(comments instanceof JCRThread))
1940        {
1941            throw new IllegalClassException(JCRThread.class, comments.getClass());
1942        }
1943
1944        JCRThread jcrComments = (JCRThread) comments;
1945
1946        JCRPost post = jcrComments.createChild(JCRPostFactory.POST_NODENAME, JCRPostFactory.POST_NODETYPE);
1947
1948        _setComment(post, comment);
1949
1950        post.setAuthor(currentUser);
1951        Date now = new Date();
1952        post.setCreationDate(now);
1953        post.setLastModified(now);
1954
1955        jcrComments.markAsRead(currentUser);
1956        jcrComments.saveChanges();
1957
1958        // Notify listeners
1959        Map<String, Object> eventParams = new HashMap<>();
1960        eventParams.put(ObservationConstants.ARGS_ID, resourceId);
1961        eventParams.put(ObservationConstants.ARGS_POST, post);
1962        _observationManager.notify(new Event(ObservationConstants.EVENT_RESOURCE_COMMENTED, currentUser, eventParams));
1963
1964        result.put("id", resourceId);
1965        result.put("commentId", post.getId());
1966
1967        return result;
1968    }
1969
1970    /**
1971     * Indicates if an user can add a comment of a given resource
1972     * 
1973     * @param user An user identity
1974     * @param resource The resource to comment
1975     * @return True if the user can add a comment
1976     */
1977    protected boolean _canAddComment(UserIdentity user, CommentableResource resource)
1978    {
1979        ExplorerNode node = resource.getParent();
1980        return _rightManager.hasRight(user, "Plugin_Explorer_File_Comment", node) == RightResult.RIGHT_ALLOW
1981                || _rightManager.hasRight(user, "Plugin_Explorer_File_Moderate_Comments", node) == RightResult.RIGHT_ALLOW;
1982    }
1983
1984    /**
1985     * Edit the comment of a resource
1986     * 
1987     * @param resourceId The id of the resource
1988     * @param commentId The id of comment to edit
1989     * @param comment The comment
1990     * @return the result
1991     */
1992    @Callable
1993    public Map<String, Object> editComment(String resourceId, String commentId, String comment)
1994    {
1995        Map<String, Object> result = new HashMap<>();
1996
1997        AmetysObject ao = _resolver.resolveById(resourceId);
1998        UserIdentity currentUser = _currentUserProvider.getUser();
1999
2000        if (!(ao instanceof CommentableResource))
2001        {
2002            throw new IllegalClassException(CommentableResource.class, ao.getClass());
2003        }
2004
2005        if (!checkLock(ao))
2006        {
2007            getLogger().warn("User '{}' is trying to edit comment '{}' on resource '{}' but it is locked by another user", currentUser, commentId, resourceId);
2008            result.put("message", "locked");
2009            return result;
2010        }
2011
2012        CommentableResource resource = (CommentableResource) ao;
2013        JCRPost post = _resolver.resolveById(commentId);
2014
2015        // Check comment rights
2016        if (!_canEditComment(currentUser, resource, post))
2017        {
2018            getLogger().warn("User '{}' is trying to add a comment on resource '{}' without the convient right.", currentUser, resourceId);
2019            result.put("message", "rights");
2020            return result;
2021        }
2022
2023        _setComment(post, comment);
2024        post.setLastModified(new Date());
2025        post.saveChanges();
2026
2027        result.put("id", resourceId);
2028        result.put("commentId", commentId);
2029
2030        return result;
2031    }
2032
2033    /**
2034     * Indicates if an user can edit a comment of a given resource
2035     * 
2036     * @param user An user identity
2037     * @param resource The resource
2038     * @param comment The comment to edit
2039     * @return True if the user can edit the comment
2040     */
2041    protected boolean _canEditComment(UserIdentity user, CommentableResource resource, JCRPost comment)
2042    {
2043        ExplorerNode node = resource.getParent();
2044        // Comment right + owner OR Moderate right
2045        return (_rightManager.hasRight(user, "Plugin_Explorer_File_Comment", node) == RightResult.RIGHT_ALLOW && comment.getAuthor().equals(user))
2046                || _rightManager.hasRight(user, "Plugin_Explorer_File_Moderate_Comments", node) == RightResult.RIGHT_ALLOW;
2047    }
2048
2049    /**
2050     * Update the content of a comment
2051     * 
2052     * @param comment The comment to update
2053     * @param content The content as string
2054     */
2055    protected void _setComment(JCRPost comment, String content)
2056    {
2057        try
2058        {
2059            ModifiableRichText richText = comment.getContent();
2060
2061            richText.setMimeType("text/plain");
2062            richText.setLastModified(new Date());
2063            richText.setInputStream(new ByteArrayInputStream(content.getBytes("UTF-8")));
2064        }
2065        catch (IOException e)
2066        {
2067            throw new AmetysRepositoryException("Failed to set post rich text", e);
2068        }
2069    }
2070
2071    /**
2072     * Get the content of a comment as a String
2073     * 
2074     * @param post the post
2075     * @return The content as String
2076     * @throws AmetysRepositoryException if failed to parse comment
2077     */
2078    protected String _getComment(JCRPost post) throws AmetysRepositoryException
2079    {
2080        try
2081        {
2082            ModifiableRichText richText = post.getContent();
2083            return IOUtils.toString(richText.getInputStream(), "UTF-8");
2084        }
2085        catch (IOException e)
2086        {
2087            throw new AmetysRepositoryException("Failed to get post rich text", e);
2088        }
2089    }
2090
2091    /**
2092     * Get the content of a comment to edit as a String
2093     * 
2094     * @param post the post
2095     * @return The content as String
2096     * @throws AmetysRepositoryException if failed to parse comment
2097     */
2098    protected String _getCommentForEditing(JCRPost post) throws AmetysRepositoryException
2099    {
2100        return _getComment(post);
2101    }
2102
2103    /**
2104     * Delete the comment of a resource
2105     * 
2106     * @param resourceId The id of the resource
2107     * @param commentId The id of comment to delete
2108     * @return the result
2109     */
2110    @Callable
2111    public Map<String, Object> deleteComment(String resourceId, String commentId)
2112    {
2113        Map<String, Object> result = new HashMap<>();
2114
2115        AmetysObject ao = _resolver.resolveById(resourceId);
2116        UserIdentity currentUser = _currentUserProvider.getUser();
2117
2118        if (!(ao instanceof CommentableResource))
2119        {
2120            throw new IllegalClassException(CommentableResource.class, ao.getClass());
2121        }
2122
2123        if (!checkLock(ao))
2124        {
2125            getLogger().warn("User '{}' is trying to delete comment '{}' on resource '{}' but it is locked by another user", currentUser, commentId, resourceId);
2126            result.put("message", "locked");
2127            return result;
2128        }
2129
2130        CommentableResource resource = (CommentableResource) ao;
2131        JCRPost post = _resolver.resolveById(commentId);
2132
2133        // Check comment rights
2134        if (!_canDeleteComment(currentUser, resource, post))
2135        {
2136            getLogger().warn("User '{}' is trying to add a comment on resource '{}' without the convient right.", currentUser, resourceId);
2137            result.put("message", "rights");
2138            return result;
2139        }
2140
2141        org.ametys.plugins.explorer.threads.Thread comments = resource.getComments(false);
2142
2143        if (!(comments instanceof JCRThread))
2144        {
2145            throw new IllegalClassException(JCRThread.class, comments.getClass());
2146        }
2147
2148        JCRThread jcrComments = (JCRThread) comments;
2149
2150        post.remove();
2151        jcrComments.saveChanges();
2152
2153        result.put("id", resourceId);
2154        result.put("commentId", commentId);
2155
2156        return result;
2157    }
2158
2159    /**
2160     * Indicates if an user can delete a comment of a given resource
2161     * 
2162     * @param user An user identity
2163     * @param resource The resource
2164     * @param comment The comment to edit
2165     * @return True if the user can edit the comment
2166     */
2167    protected boolean _canDeleteComment(UserIdentity user, CommentableResource resource, JCRPost comment)
2168    {
2169        ExplorerNode node = resource.getParent();
2170        // Comment right + owner OR Moderate right
2171        return (_rightManager.hasRight(user, "Plugin_Explorer_File_Comment", node) == RightResult.RIGHT_ALLOW && comment.getAuthor().equals(user))
2172                || _rightManager.hasRight(user, "Plugin_Explorer_File_Moderate_Comments", node) == RightResult.RIGHT_ALLOW;
2173    }
2174
2175    /**
2176     * Get the comments of a resource
2177     * 
2178     * @param resourceId The id of the resource
2179     * @param isEdition true to get the comments in edit mode
2180     * @return The comments
2181     */
2182    @Callable
2183    public List<Map<String, Object>> getComments(String resourceId, boolean isEdition)
2184    {
2185        List<Map<String, Object>> comments = new ArrayList<>();
2186
2187        AmetysObject ao = _resolver.resolveById(resourceId);
2188
2189        if (!(ao instanceof CommentableResource))
2190        {
2191            throw new IllegalClassException(CommentableResource.class, ao.getClass());
2192        }
2193
2194        org.ametys.plugins.explorer.threads.Thread thread = ((CommentableResource) ao).getComments(false);
2195        if (thread != null)
2196        {
2197            if (!(thread instanceof JCRThread))
2198            {
2199                throw new IllegalClassException(JCRThread.class, thread.getClass());
2200            }
2201
2202            JCRThread jcrComments = (JCRThread) thread;
2203
2204            AmetysObjectIterable<AmetysObject> children = jcrComments.getChildren();
2205            for (AmetysObject child : children)
2206            {
2207                if (child instanceof JCRPost)
2208                {
2209                    JCRPost comment = (JCRPost) child;
2210                    comments.add(_comment2json(comment, isEdition));
2211                }
2212            }
2213        }
2214
2215        return comments;
2216    }
2217
2218    /**
2219     * Get a comment of a resource
2220     * 
2221     * @param commentId The id of the comment
2222     * @param isEdition true to get the comment in edit mode
2223     * @return The comments
2224     */
2225    @Callable
2226    public Map<String, Object> getComment(String commentId, boolean isEdition)
2227    {
2228        // FIXME check read access on resource?
2229        JCRPost comment = _resolver.resolveById(commentId);
2230        return _comment2json(comment, isEdition);
2231    }
2232
2233    private Map<String, Object> _comment2json(JCRPost comment, boolean isEdition)
2234    {
2235        Map<String, Object> result = new HashMap<>();
2236
2237        result.put("id", comment.getId());
2238        result.put("content", isEdition ? _getCommentForEditing(comment) : _getComment(comment));
2239        result.put("author", _userHelper.user2json(comment.getAuthor()));
2240        result.put("creationDate", DateUtils.dateToString(comment.getCreationDate()));
2241        result.put("lastModifiedDate", DateUtils.dateToString(comment.getLastModified()));
2242
2243        UserIdentity author = comment.getAuthor();
2244        boolean isOwner = author.equals(_currentUserProvider.getUser());
2245        result.put("isOwner", isOwner);
2246
2247        return result;
2248    }
2249
2250    private static class VersionInformation
2251    {
2252        private String _rawName;
2253
2254        private String _name;
2255
2256        private Date _creationDate;
2257
2258        private Set<String> _labels = new HashSet<>();
2259
2260        /**
2261         * Creates a {@link VersionInformation}.
2262         * 
2263         * @param rawName the revision name.
2264         * @param creationDate the revision creation date.
2265         * @throws RepositoryException if an error occurs.
2266         */
2267        public VersionInformation(String rawName, Date creationDate) throws RepositoryException
2268        {
2269            _creationDate = creationDate;
2270            _rawName = rawName;
2271            // 1.0 > v0, 1.1 > v1, ...
2272            _name = String.valueOf(Integer.parseInt(_rawName.substring(2)) + 1);
2273        }
2274
2275        /**
2276         * Retrieves the version name.
2277         * 
2278         * @return the version name.
2279         */
2280        public String getVersionName()
2281        {
2282            return _name;
2283        }
2284
2285        /**
2286         * Retrieves the version raw name.
2287         * 
2288         * @return the version raw name.
2289         */
2290        public String getVersionRawName()
2291        {
2292            return _rawName;
2293        }
2294
2295        /**
2296         * Retrieves the creation date.
2297         * 
2298         * @return the creation date.
2299         * @throws RepositoryException if an error occurs.
2300         */
2301        public Date getCreatedAt() throws RepositoryException
2302        {
2303            return _creationDate;
2304        }
2305
2306        /**
2307         * Retrieves the labels associated with this version.
2308         * 
2309         * @return the labels.
2310         */
2311        public Set<String> getLabels()
2312        {
2313            return _labels;
2314        }
2315
2316        /**
2317         * Add a label to this version.
2318         * 
2319         * @param label the label.
2320         */
2321        public void addLabel(String label)
2322        {
2323            _labels.add(label);
2324        }
2325    }
2326}