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