001/*
002 *  Copyright 2015 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.plugins.workspaces.documents;
017
018import java.awt.image.BufferedImage;
019import java.io.IOException;
020import java.io.InputStream;
021import java.util.ArrayList;
022import java.util.Arrays;
023import java.util.Collection;
024import java.util.Collections;
025import java.util.HashMap;
026import java.util.LinkedList;
027import java.util.List;
028import java.util.Map;
029import java.util.Objects;
030import java.util.Optional;
031import java.util.function.Function;
032import java.util.stream.Collectors;
033
034import javax.jcr.RepositoryException;
035
036import org.apache.avalon.framework.service.ServiceException;
037import org.apache.avalon.framework.service.ServiceManager;
038import org.apache.cocoon.components.ContextHelper;
039import org.apache.cocoon.environment.Request;
040import org.apache.cocoon.servlet.multipart.Part;
041import org.apache.commons.io.IOUtils;
042import org.apache.commons.lang.IllegalClassException;
043import org.apache.commons.lang3.StringUtils;
044import org.apache.excalibur.source.Source;
045import org.apache.excalibur.source.SourceResolver;
046
047import org.ametys.cms.content.indexing.solr.SolrFieldNames;
048import org.ametys.cms.search.query.DocumentTypeQuery;
049import org.ametys.cms.search.query.FilenameQuery;
050import org.ametys.cms.search.query.FullTextQuery;
051import org.ametys.cms.search.query.MatchAllQuery;
052import org.ametys.cms.search.query.MimeTypeGroupQuery;
053import org.ametys.cms.search.query.OrQuery;
054import org.ametys.cms.search.query.Query;
055import org.ametys.cms.search.query.Query.Operator;
056import org.ametys.cms.search.query.StringQuery;
057import org.ametys.cms.search.solr.SearcherFactory;
058import org.ametys.cms.transformation.xslt.ResolveURIComponent;
059import org.ametys.core.right.RightManager.RightResult;
060import org.ametys.core.ui.Callable;
061import org.ametys.core.user.User;
062import org.ametys.core.user.UserIdentity;
063import org.ametys.core.user.UserManager;
064import org.ametys.core.util.DateUtils;
065import org.ametys.core.util.FilenameUtils;
066import org.ametys.core.util.URIUtils;
067import org.ametys.plugins.explorer.ExplorerNode;
068import org.ametys.plugins.explorer.ModifiableExplorerNode;
069import org.ametys.plugins.explorer.cmis.CMISRootResourcesCollection;
070import org.ametys.plugins.explorer.resources.ModifiableResource;
071import org.ametys.plugins.explorer.resources.ModifiableResourceCollection;
072import org.ametys.plugins.explorer.resources.Resource;
073import org.ametys.plugins.explorer.resources.ResourceCollection;
074import org.ametys.plugins.explorer.resources.actions.AddOrUpdateResourceHelper;
075import org.ametys.plugins.explorer.resources.actions.AddOrUpdateResourceHelper.ResourceOperationMode;
076import org.ametys.plugins.explorer.resources.actions.AddOrUpdateResourceHelper.ResourceOperationResult;
077import org.ametys.plugins.explorer.resources.actions.ExplorerResourcesDAO;
078import org.ametys.plugins.explorer.resources.jcr.JCRResource;
079import org.ametys.plugins.explorer.resources.jcr.JCRResourcesCollection;
080import org.ametys.plugins.explorer.threads.jcr.JCRPost;
081import org.ametys.plugins.repository.AmetysObject;
082import org.ametys.plugins.repository.AmetysObjectIterable;
083import org.ametys.plugins.repository.AmetysRepositoryException;
084import org.ametys.plugins.repository.ModifiableAmetysObject;
085import org.ametys.plugins.repository.RemovableAmetysObject;
086import org.ametys.plugins.repository.jcr.JCRTraversableAmetysObject;
087import org.ametys.plugins.repository.lock.LockableAmetysObject;
088import org.ametys.plugins.workspaces.WorkspacesHelper;
089import org.ametys.plugins.workspaces.html.HTMLTransformer;
090import org.ametys.plugins.workspaces.indexing.solr.SolrWorkspacesConstants;
091import org.ametys.plugins.workspaces.project.ProjectManager;
092import org.ametys.plugins.workspaces.project.modules.WorkspaceModuleExtensionPoint;
093import org.ametys.plugins.workspaces.project.objects.Project;
094import org.ametys.plugins.workspaces.search.query.KeywordQuery;
095import org.ametys.plugins.workspaces.search.query.ProjectQuery;
096import org.ametys.runtime.plugin.component.PluginAware;
097import org.ametys.web.WebConstants;
098
099/**
100 * DAO for resources of a project
101 *
102 */
103public class WorkspaceExplorerResourceDAO extends ExplorerResourcesDAO implements PluginAware
104{
105    /** Avalon Role */
106    @SuppressWarnings("hiding")
107    public static final String ROLE = WorkspaceExplorerResourceDAO.class.getName();
108    
109    /**
110     * Enumeration for resource type
111     *
112     */
113    public static enum ResourceType
114    {
115        /** Folder */
116        FOLDER,
117        /** File */
118        FILE
119    }
120    
121    /** resource operation helper */
122    protected AddOrUpdateResourceHelper _addOrUpdateResourceHelper;
123    
124    /** The project manager */
125    private ProjectManager _projectManager;
126    
127    private HTMLTransformer _htmlTransformer;
128    private SourceResolver _sourceResolver;
129    private String _pluginName;
130
131    private SearcherFactory _searcherFactory;
132
133    private UserManager _userManager;
134
135    private WorkspaceModuleExtensionPoint _moduleEP;
136
137    private WorkspacesHelper _workspaceHelper;
138
139    @Override
140    public void service(ServiceManager manager) throws ServiceException
141    {
142        super.service(manager);
143        _projectManager = (ProjectManager) manager.lookup(ProjectManager.ROLE);
144        _htmlTransformer = (HTMLTransformer) manager.lookup(HTMLTransformer.ROLE);
145        _sourceResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE);
146        _addOrUpdateResourceHelper = (AddOrUpdateResourceHelper) manager.lookup(AddOrUpdateResourceHelper.ROLE);
147        _searcherFactory = (SearcherFactory) manager.lookup(SearcherFactory.ROLE);
148        _userManager = (UserManager) manager.lookup(UserManager.ROLE);
149        _moduleEP = (WorkspaceModuleExtensionPoint) manager.lookup(WorkspaceModuleExtensionPoint.ROLE);
150        _workspaceHelper = (WorkspacesHelper) manager.lookup(WorkspacesHelper.ROLE);
151    }
152    
153    @Override
154    public void setPluginInfo(String pluginName, String featureName, String id)
155    {
156        _pluginName = pluginName;
157    }
158    
159    /**
160     * Add a folder
161     * @param parentId Identifier of the parent collection. Can be null to add folder to root folder.
162     * @param inputName The desired name
163     * @param description The folder description
164     * @return The created folder or an error if a folder with same name already exists.
165     * @throws IllegalAccessException If the user has no sufficient rights
166     */
167    @Callable
168    public Map<String, Object> addFolder(String parentId, String inputName, String description) throws IllegalAccessException
169    {
170        return addFolder(parentId, inputName, description, false);
171    }
172    
173    /**
174     * Add a folder
175     * @param parentId Identifier of the parent collection. Can be null to add folder to root folder.
176     * @param inputName The desired name
177     * @param description The folder description
178     * @param renameIfExists True to rename if existing
179     * @return The result map with id, parentId and name keys
180     * @throws IllegalAccessException If the user has no sufficient rights
181     */
182    @Callable
183    public Map<String, Object> addFolder(String parentId, String inputName, String description, Boolean renameIfExists) throws IllegalAccessException
184    {
185        ResourceCollection document = _getRootIfNull(parentId);
186        if (document == null)
187        {
188            throw new IllegalArgumentException("Unable to add folder: parent folder not found");
189        }
190        
191        // TODO catch IllegalAccessException -> error = has-right
192        
193        if (!(document instanceof ModifiableResourceCollection))
194        {
195            throw new IllegalClassException(ModifiableResourceCollection.class, document.getClass());
196        }
197        
198        List<String> errors = new LinkedList<>();
199        ResourceCollection collection = addResourceCollection((ModifiableResourceCollection) document, inputName, renameIfExists, errors);
200        
201        Map<String, Object> result = new HashMap<>();
202        if (!errors.isEmpty())
203        {
204            result.put("message", errors.get(0));
205            result.put("error", true);
206        }
207        
208        if (collection != null)
209        {
210            ((ModifiableResourceCollection) collection).setDescription(description);
211            ((ModifiableResourceCollection) collection).saveChanges();
212            
213            result.putAll(_extractFolderData(collection));
214        }
215        
216        return result;
217    }
218    
219    /**
220     * Rename a folder
221     * @param id Identifier of the folder to edit
222     * @param name The new name
223     * @return The result map with id and name keys
224     * @throws IllegalAccessException If the user has no sufficient rights
225     */
226    @Callable
227    public Map<String, Object> renameFolder(String id, String name) throws IllegalAccessException
228    {
229        Map<String, Object> result = new HashMap<>();
230        
231        JCRResourcesCollection folder = (JCRResourcesCollection) _resolver.resolveById(id);
232        
233        // TODO catch IllegalAccessException -> error = has-right
234        
235        List<String> errors = new LinkedList<>();
236        JCRResourcesCollection newFolder = null;
237        try
238        {
239            newFolder = (JCRResourcesCollection) renameObject(folder, name, errors);
240            if (!errors.isEmpty())
241            {
242                result.put("success", false);
243                result.put("message", errors.get(0));
244            }
245            else
246            {
247                newFolder.saveChanges();
248
249                result.put("success", true);
250                result.putAll(_extractFolderData(newFolder));
251            }
252        }
253        catch (RepositoryException e)
254        {
255            getLogger().error("Repository exception during folder edition.", e);
256            errors.add("repository");
257        }
258        
259        return result;
260    }
261    
262    /**
263     * Edit a folder
264     * @param id Identifier of the folder to edit
265     * @param inputName The desired name
266     * @param description The folder description
267     * @return The result map with id and name keys
268     * @throws IllegalAccessException If the user has no sufficient rights
269     */
270    @Callable
271    public Map<String, Object> editFolder(String id, String inputName, String description) throws IllegalAccessException
272    {
273        JCRResourcesCollection folder = (JCRResourcesCollection) _resolver.resolveById(id);
274        
275        // TODO catch IllegalAccessException -> error = has-right
276        
277        List<String> errors = new LinkedList<>();
278        JCRResourcesCollection newFolder = null;
279        try
280        {
281            newFolder = (JCRResourcesCollection) renameObject(folder, inputName, errors);
282        }
283        catch (RepositoryException e)
284        {
285            getLogger().error("Repository exception during folder edition.", e);
286            errors.add("repository");
287        }
288        
289        Map<String, Object> result = new HashMap<>();
290        if (!errors.isEmpty())
291        {
292            String error = errors.get(0);
293            
294            // existing node is allowed, the description can still be changed.
295            if (!"already-exist".equals(error))
296            {
297                result.put("message", error);
298            }
299            else
300            {
301                newFolder = folder;
302            }
303        }
304        
305        if (newFolder != null)
306        {
307            newFolder.setDescription(description);
308            newFolder.saveChanges();
309            
310            result.putAll(_extractFolderData(newFolder));
311        }
312        
313        return result;
314    }
315    
316    /**
317     * Delete a folder
318     * @param id Identifier of the folder to delete
319     * @return The result map with the parent id key
320     * @throws IllegalAccessException If the user has no sufficient rights
321     */
322    @Callable
323    public Map<String, Object> deleteFolder(String id) throws IllegalAccessException
324    {
325        RemovableAmetysObject folder = (RemovableAmetysObject) _resolver.resolveById(id);
326        
327        // TODO catch IllegalAccessException -> error = has-right
328        
329        List<String> errors = new LinkedList<>();
330        String parentId = deleteObject(folder, errors);
331        
332        Map<String, Object> result = new HashMap<>();
333        if (!errors.isEmpty())
334        {
335            String error = errors.get(0);
336            result.put("message", error);
337        }
338        
339        // include parent id except if it is the root
340        ResourceCollection rootCollection = _getRootFromRequest();
341        boolean isRoot = rootCollection != null && rootCollection.getId().equals(parentId);
342        if (StringUtils.isNotEmpty(parentId) && !isRoot)
343        {
344            result.put("parentId", parentId);
345        }
346        
347        return result;
348    }
349    
350    /**
351     * Lock resources
352     * @param ids The id of resources to lock
353     * @return the result.
354     */
355    @Callable
356    public Map<String, Object> lockResources(List<String> ids)
357    {
358        Map<String, Object> result = new HashMap<>();
359        
360        result.put("locked-resources", new ArrayList<String>());
361        result.put("unlocked-resources", new ArrayList<Map<String, Object>>());
362        
363        for (String id : ids)
364        {
365            JCRResource resource = _resolver.resolveById(id);
366            
367            if (!resource.isLocked())
368            {
369                resource.lock();
370                
371                @SuppressWarnings("unchecked")
372                List<String> lockedResources = (List<String>) result.get("locked-resources");
373                lockedResources.add(id);
374            }
375            else if (!resource.getLockOwner().equals(_currentUserProvider.getUser()))
376            {
377                UserIdentity lockOwner = resource.getLockOwner();
378                
379                getLogger().error("Unable to lock resource of id '" + id + "': the resource is already locked by user " + UserIdentity.userIdentityToString(lockOwner));
380                
381                Map<String, Object> info = new HashMap<>();
382                info.put("id", id);
383                info.put("name", resource.getName());
384                info.put("lockOwner", _userHelper.user2json(lockOwner));
385                
386                @SuppressWarnings("unchecked")
387                List<Map<String, Object>> unlockedResources = (List<Map<String, Object>>) result.get("unlocked-resources");
388                unlockedResources.add(info);
389            }
390        }
391        
392        return result;
393    }
394    
395    /**
396     * Unlock resources
397     * @param ids The id of resources to lock
398     * @return the result.
399     */
400    @Callable
401    public Map<String, Object> unlockResources(List<String> ids)
402    {
403        UserIdentity currentUser = _currentUserProvider.getUser();
404        boolean canUnlockAll = _rightManager.hasRight(currentUser, RIGHTS_RESOURCE_UNLOCK_ALL, "/cms") == RightResult.RIGHT_ALLOW;
405        
406        Map<String, Object> result = new HashMap<>();
407        
408        result.put("unlocked-resources", new ArrayList<String>());
409        result.put("still-locked-resources", new ArrayList<Map<String, Object>>());
410        
411        for (String id : ids)
412        {
413            JCRResource resource = _resolver.resolveById(id);
414            
415            if (resource.isLocked())
416            {
417                if (canUnlockAll || resource.getLockOwner().equals(currentUser))
418                {
419                    resource.unlock();
420                    
421                    @SuppressWarnings("unchecked")
422                    List<String> unlockedResources = (List<String>) result.get("unlocked-resources");
423                    unlockedResources.add(id);
424                }
425                else
426                {
427                    UserIdentity lockOwner = resource.getLockOwner();
428                    
429                    getLogger().error("Unable to unlock resource of id '" + id + "': the resource is locked by user " + UserIdentity.userIdentityToString(lockOwner));
430                    
431                    Map<String, Object> info = new HashMap<>();
432                    info.put("id", id);
433                    info.put("name", resource.getName());
434                    info.put("lockOwner", _userHelper.user2json(lockOwner));
435                    
436                    @SuppressWarnings("unchecked")
437                    List<Map<String, Object>> stilllockedResources = (List<Map<String, Object>>) result.get("still-locked-resources");
438                    stilllockedResources.add(info);
439                }
440            }
441            else
442            {
443                @SuppressWarnings("unchecked")
444                List<String> unlockedResources = (List<String>) result.get("unlocked-resources");
445                unlockedResources.add(id);
446            }
447        }
448        
449        return result;
450    }
451    
452    /**
453     * Determines if a resource with given name already exists
454     * @param parentId the id of parent collection. Can be null.
455     * @param name the name of resource
456     * @return true if a resource with same name exists
457     */
458    @Callable
459    @Override
460    public boolean resourceExists(String parentId, String name)
461    {
462        ResourceCollection folder = _getRootIfNull(parentId);
463        return folder != null && resourceExists(folder, name);
464    }
465    
466    /**
467     * Rename a folder
468     * @param id Identifier of the folder to edit
469     * @param name The new name
470     * @return The result map with id and name keys
471     * @throws IllegalAccessException If the user has no sufficient rights
472     */
473    @Callable
474    public Map<String, Object> renameFile(String id, String name) throws IllegalAccessException
475    {
476        Map<String, Object> result = new HashMap<>();
477        
478        JCRResource file = (JCRResource) _resolver.resolveById(id);
479        if (StringUtils.isNotEmpty(name) && !StringUtils.equals(file.getName(), name))
480        {
481            // TODO catch IllegalAccessException -> error = has-right
482            
483            List<String> errors = new LinkedList<>();
484            JCRResource renamedFile = null;
485            try
486            {
487                renamedFile = renameResource(file, name, errors);
488                if (!errors.isEmpty())
489                {
490                    result.put("success", false);
491                    result.put("message", errors.get(0));
492                }
493                else
494                {
495                    renamedFile.saveChanges();
496
497                    result.put("success", true);
498                    result.putAll(_extractFileData(renamedFile));
499                }
500            }
501            catch (RepositoryException e)
502            {
503                getLogger().error("Repository exception during file edition.", e);
504                errors.add("repository");
505            }
506            
507        }
508        
509        return result;
510    }
511    
512    
513    /**
514     * Edit a file
515     * @param id Identifier of the file to edit
516     * @param inputName The desired name
517     * @param description The file description
518     * @param tags The file tags 
519     * @return The result map with id and name keys
520     * @throws IllegalAccessException If the user has no sufficient rights
521     */
522    @Callable
523    public Map<String, Object> editFile(String id, String inputName, String description, Collection<String> tags) throws IllegalAccessException
524    {
525        Map<String, Object> result = new HashMap<>();
526        JCRResource file = (JCRResource) _resolver.resolveById(id);
527        
528        // Check lock on resource
529        if (!checkLock(file))
530        {
531            getLogger().warn("User '{}' is trying to edit file '{}' but it is locked by another user", _currentUserProvider.getUser(), file.getName());
532            result.put("message", "locked");
533            return result;
534        }
535        
536        // TODO catch IllegalAccessException -> error = has-right
537        
538        List<String> errors = new LinkedList<>();
539        
540        // Rename
541        JCRResource renamedFile = null;
542        if (StringUtils.isNotEmpty(inputName) && !StringUtils.equals(file.getName(), inputName))
543        {
544            try
545            {
546                renamedFile = renameResource(file, inputName, errors);
547                
548                if (errors.isEmpty())
549                {
550                    file = renamedFile;
551                }
552            }
553            catch (RepositoryException e)
554            {
555                getLogger().error("Repository exception during folder edition.", e);
556                errors.add("repository-rename");
557            }
558        }
559        
560        if (!errors.isEmpty())
561        {
562            String error = errors.get(0);
563            result.put("message", error);
564            return result;
565        }
566        
567        // edit description + tags
568        List<String> fileTags = _sanitizeFileTags(tags);
569        
570        Map<String, Object> editValues = new HashMap<>();
571        editValues.put("dc_description", StringUtils.defaultIfEmpty(description, null));
572        
573        try
574        {
575            file.setKeywords(fileTags.toArray(new String[fileTags.size()]));
576            setDCMetadata(file, editValues);
577            
578            // Add tags to the project
579            _projectManager.addTags(fileTags);
580            
581            file.saveChanges();
582        }
583        catch (AmetysRepositoryException e)
584        {
585            getLogger().error("Repository exception during folder edition.", e);
586            errors.add("repository-edit");
587        }
588        
589        if (!errors.isEmpty())
590        {
591            String error = errors.get(0);
592            result.put("message", error);
593        }
594        else
595        {
596            result.putAll(_extractFileData(file));
597        }
598        
599        return result;
600    }
601    
602    /**
603     * Set tags to resource
604     * @param resourceId the id of resources
605     * @param tagNames the tags (keywords)
606     */
607    @Callable
608    public void setTags(String resourceId, List<String> tagNames)
609    {
610        JCRResource file = (JCRResource) _resolver.resolveById(resourceId);
611        
612        List<String> tags = _sanitizeFileTags(tagNames);
613        
614        // Add tags to projects' root
615        _projectManager.addTags(tags);
616        
617        file.setKeywords(tags.toArray(new String[tags.size()]));
618        
619        file.saveChanges();
620    }
621    
622    /**
623     * Copy file resources
624     * @param ids The list of identifiers for the resources to copy
625     * @param targetId The id of target to copy into. Can be null to copy into root folder.
626     * @return The result map with a message key in case of an error or with the list of uncopied/copied resources
627     * @throws RepositoryException If there is a repository error
628     */
629    @Callable
630    public Map<String, Object> copyFiles(List<String> ids, String targetId) throws RepositoryException
631    {
632        ResourceCollection folder = _getRootIfNull(targetId);
633        if (folder == null)
634        {
635            throw new IllegalArgumentException("Unable to copy files: parent folder not found");
636        }
637        
638        if (!(folder instanceof ModifiableResourceCollection))
639        {
640            throw new IllegalClassException(ModifiableResourceCollection.class, folder.getClass());
641        }
642        
643        return copyResource(ids, (ModifiableResourceCollection) folder);
644    }
645    
646    /**
647     * Move documents (files or folders)
648     * @param ids The list of identifiers for the objects to move
649     * @param targetId The id of target to move into. Can be null to move documents to root folder.
650     * @return The result map with a message key in case of an error or with the list of unmoved/moved objects
651     * @throws RepositoryException If there is a repository error
652     */
653    @Callable
654    public Map<String, Object> moveDocuments(List<String> ids, String targetId) throws RepositoryException
655    {
656        ResourceCollection folder = _getRootIfNull(targetId);
657        if (folder == null)
658        {
659            throw new IllegalArgumentException("Unable to move documents: parent folder not found");
660        }
661        
662        if (!(folder instanceof JCRTraversableAmetysObject))
663        {
664            throw new IllegalClassException(JCRTraversableAmetysObject.class, folder.getClass());
665        }
666        
667        return moveObject(ids, (JCRTraversableAmetysObject) folder);
668    }
669    
670    /**
671     * Search for files in the document module
672     * @param query The search query
673     * @param lang The search language
674     * @return A result map containing a <em>resources</em> entry which is a list of the files data
675     * @throws Exception if an exception occurs
676     */
677    @Callable
678    public Map<String, Object> searchFiles(String query, String lang) throws Exception
679    {
680        // Handle result map
681        Map<String, Object> result = new HashMap<>();
682        
683        String escapedQuery = query.replace("\"", "\\\"");
684        
685        Query solrQuery;
686        if (StringUtils.isEmpty(query))
687        {
688            solrQuery = new MatchAllQuery();
689        }
690        else
691        {
692            List<Query> queries = new ArrayList<>();
693            queries.add(new FilenameQuery(query));
694            queries.add(new StringQuery(SolrFieldNames.TITLE, Operator.LIKE, "*" + query + "*", null));
695            queries.add(new FullTextQuery(escapedQuery, lang));
696            queries.add(new KeywordQuery(escapedQuery.split(" ")));
697            solrQuery = new OrQuery(queries);
698        }
699        
700        AmetysObjectIterable<Resource> results = _searcherFactory.create()
701                .withQuery(solrQuery)
702                .addFilterQuery(new DocumentTypeQuery(SolrWorkspacesConstants.TYPE_PROJECT_RESOURCE))
703                .addFilterQuery(new ProjectQuery(_getProjectFromRequest().getId()))
704                .search();
705        
706        List<Map<String, Object>> resourceData = results.stream()
707            .map(this::_extractFileData)
708            .collect(Collectors.toList());
709        
710        result.put("resources", resourceData);
711        
712        return result;
713    }
714    
715    /**
716     * Search for files by their type
717     * @param type The file type
718     * @return A result map containing a <em>resources</em> entry which is a list of the files data
719     * @throws Exception if an exception occurs
720     */
721    @Callable
722    public Map<String, Object> searchFilesByType(String type) throws Exception
723    {
724        Map<String, Object> result = new HashMap<>();
725        
726        AmetysObjectIterable<Resource> results = _searcherFactory.create()
727                .withQuery(new MimeTypeGroupQuery(type))
728                .addFilterQuery(new DocumentTypeQuery(SolrWorkspacesConstants.TYPE_PROJECT_RESOURCE))
729                .addFilterQuery(new ProjectQuery(_getProjectFromRequest().getId()))
730                .search();
731        
732        List<Map<String, Object>> resourceData = results.stream()
733            .map(this::_extractFileData)
734            .collect(Collectors.toList());
735        
736        result.put("resources", resourceData);
737        
738        return result;
739    }
740    
741    /**
742     * Get the root folder
743     * @return the root folder as JSON object
744     */
745    @Callable
746    public Map<String, Object> getRootFolder()
747    {
748        return getFolder(null);
749    }
750    
751    /**
752     * Get folder
753     * @param folderId the folder id
754     * @return the folder as JSON object
755     */
756    @Callable
757    public Map<String, Object> getFolder(String folderId)
758    {
759        ResourceCollection collection = _getRootIfNull(folderId);
760        return _extractFolderData(collection);
761    }
762    
763    /**
764     * Get file
765     * @param resourceId the resource id
766     * @return the file as JSON object
767     */
768    @Callable
769    public Map<String, Object> getFile(String resourceId)
770    {
771        Resource resource = _resolver.resolveById(resourceId);
772        return _extractFileData(resource);
773    }
774    /**
775     * Get the child folders
776     * @param parentId the parent id. Can be null to get root folders
777     * @return the sub folders
778     */
779    @Callable
780    public List<Map<String, Object>> getFolders(String parentId)
781    {
782        return getChildDocumentsData(parentId, false, true);
783    }
784    
785    /**
786     * Get the child files
787     * @param parentId the parent id. Can be null to get root files
788     * @return the child files
789     */
790    @Callable
791    public List<Map<String, Object>> getFiles(String parentId)
792    {
793        return getChildDocumentsData(parentId, true, false);
794    }
795    
796    /**
797     * Get the child folders and files
798     * @param folderId the parent id. Can be null to get root folders and files
799     * @return the child folders and files
800     */
801    @Callable
802    public Map<String, Object> getFoldersAndFiles(String folderId)
803    {
804        ResourceCollection collection = _getRootIfNull(folderId);
805        
806        Map<String, Object> data = _extractFolderData(collection);
807        
808        data.put("files", getChildDocumentsData(folderId, true, false));
809        data.put("children", getChildDocumentsData(folderId, false, true));
810        
811        return data;
812    }
813    
814    
815    /**
816     * Retrieves the children of a document and extracts its data.
817     * @param parentId Identifier of the parent collection. Can be null to get children of root folder.
818     * @param excludeFolders Folders will be excluded if true
819     * @param excludeFiles Files will be excluded if true
820     * @return The map of information
821     */
822    @Callable
823    public List<Map<String, Object>> getChildDocumentsData(String parentId, boolean excludeFolders, boolean excludeFiles)
824    {
825        ResourceCollection document = _getRootIfNull(parentId);
826        if (document == null)
827        {
828            throw new IllegalArgumentException("Unable to get child documents: parent folder not found");
829        }
830        return getChildDocumentsData(document, excludeFolders, excludeFiles);
831    }
832    
833    /**
834     * Retrieves the children of a document and extracts its data.
835     * @param document the document
836     * @param excludeFolders Folders will be excluded if true
837     * @param excludeFiles Files will be excluded if true
838     * @return The map of information
839     */
840    public List<Map<String, Object>> getChildDocumentsData(ResourceCollection document, boolean excludeFolders, boolean excludeFiles)
841    {
842        ResourceCollection parent = _getRootIfNull(document);
843        if (parent == null)
844        {
845            throw new IllegalArgumentException("Unable to get child documents: parent folder not found");
846        }
847        
848        return parent.getChildren().stream()
849            .map(child -> _extractDocumentData(child, excludeFolders, excludeFiles))
850            .filter(Objects::nonNull)
851            .collect(Collectors.toList());
852    }
853    
854    /**
855     * Retrieves the set of standard data for a document (folder or resource)
856     * @param id the document id or null to get root document
857     * @param excludeFilesInFolderHierarchy Should child files be taken into account when extracting data of a folder
858     * @return The map of data
859     */
860    // TODO To remove not used
861    @Deprecated
862    @Callable
863    public Map<String, Object> getDocumentData(String id, boolean excludeFilesInFolderHierarchy)
864    {
865        AmetysObject document = id == null ? _getRootFromRequest() : _resolver.resolveById(id);
866        if (document == null)
867        {
868            throw new IllegalArgumentException("No project found in request to get root document data");
869        }
870        
871        Map<String, Object> data = _extractDocumentData(document, false, false);
872        return data != null ? data : new HashMap<>();
873    }
874    
875    /**
876     * Retrieves the set of standard data for a list of documents
877     * @param ids The list of document identifiers
878     * @param excludeFilesInFolderHierarchy Should child files be taken into account when extracting data of a folder 
879     * @return The map of data
880     */
881    // TODO To remove not used
882    @Callable
883    public List<Map<String, Object>> getDocumentsData(List<String> ids, boolean excludeFilesInFolderHierarchy)
884    {
885        return ids.stream()
886            .map(id -> getDocumentData(id, excludeFilesInFolderHierarchy))
887            .collect(Collectors.toList());
888    }
889    
890    @Override
891    protected Map<String, Object> _comment2json(JCRPost comment, boolean isEdition)
892    {
893        Map<String, Object> comment2json = super._comment2json(comment, isEdition);
894        
895        String lang = _getCurrentLanguage();
896        String authorImgUrl = _workspaceHelper.getAvatar(comment.getAuthor(), lang, 30);
897        
898        @SuppressWarnings("unchecked")
899        Map<String, Object> author = (Map<String, Object>) comment2json.get("author");
900        author.put("imgUrl", authorImgUrl);
901        
902        return comment2json;
903    }
904    
905    @Override
906    protected Map<String, Object> _version2json(JCRResource resource, VersionInformation versionInformation) throws RepositoryException
907    {
908        Map<String, Object> version2json =  super._version2json(resource, versionInformation);
909        
910        String lang = _getCurrentLanguage();
911        
912        @SuppressWarnings("unchecked")
913        Map<String, Object> author = (Map<String, Object>) version2json.get("author");
914        String authorImgUrl = _workspaceHelper.getAvatar(resource.getLastContributor(), lang, 30);
915        author.put("imgUrl", authorImgUrl);
916        
917        return version2json;
918        
919    }
920    
921    /**
922     * Generates an uri to open a document through webdav
923     * @param documentId The document identifier
924     * @return The generated uri
925     */
926    @Callable
927    public String generateWebdavUri(String documentId)
928    {
929        return ResolveURIComponent.resolve("webdav-project-resource", documentId, false, true);
930    }
931    
932    private ResourceCollection _getRootIfNull(ResourceCollection document)
933    {
934        return document != null ? document : _getRootFromRequest();
935    }
936    
937    private ResourceCollection _getRootIfNull(String documentId)
938    {
939        return StringUtils.isNotEmpty(documentId)
940                ? (ResourceCollection) _resolver.resolveById(documentId)
941                : _getRootFromRequest();
942    }
943    
944    private ResourceCollection _getRootFromRequest()
945    {
946        Project project = _getProjectFromRequest();
947        
948        if (project != null)
949        {
950            return _getRootFromProject(project);
951        }
952        else
953        {
954            return null;
955        }
956    }
957    
958    private ResourceCollection _getRootFromObject(AmetysObject ametysObject)
959    {
960        Project project = _getProjectFomObject(ametysObject);
961        if (project != null)
962        {
963            return _getRootFromProject(project);
964        }
965        return null;
966    }
967    
968    private ResourceCollection _getRootFromProject(Project project)
969    {
970        if (project != null)
971        {
972            DocumentWorkspaceModule module = _moduleEP.getModule(DocumentWorkspaceModule.DODUMENT_MODULE_ID);
973            return module.getModuleRoot(project, false);
974        }
975        else
976        {
977            return null;
978        }
979    }
980    
981    private Project _getProjectFromRequest()
982    {
983        Request request = ContextHelper.getRequest(_context);
984        
985        String projectName = (String) request.getAttribute("projectName");
986        if (projectName != null)
987        {
988            return _projectManager.getProject(projectName);
989        }
990        else
991        {
992            return null;
993        }
994    }
995    
996    private Project _getProjectFomObject(AmetysObject ametysObject)
997    {
998        AmetysObject parent = ametysObject;
999        
1000        while (parent != null && !(parent instanceof Project))
1001        {
1002            parent = parent.getParent();
1003        }
1004        
1005        if (parent == null)
1006        {
1007            return null;
1008        }
1009        return (Project) parent;
1010    }
1011    
1012    private String _getCurrentLanguage()
1013    {
1014        Request request = ContextHelper.getRequest(_context);
1015        return (String) request.getAttribute(WebConstants.REQUEST_ATTR_SITEMAP_NAME);
1016    }
1017    
1018    /**
1019     * Internal method to extract the valuable data of a document
1020     * @param document The document (file or folder)
1021     * @param excludeFolders Folders will be excluded if true
1022     * @param excludeFiles Files will be excluded if true
1023     * @return the valuable document data
1024     */
1025    protected Map<String, Object> _extractDocumentData(AmetysObject document, boolean excludeFolders, boolean excludeFiles)
1026    {
1027        if (!excludeFiles && document instanceof Resource)
1028        {
1029            Resource file = (Resource) document;
1030            if (_canView(file))
1031            {
1032                return _extractFileData(file);
1033            }
1034        }
1035        else if (!excludeFolders && document instanceof ResourceCollection)
1036        {
1037            ResourceCollection folder = (ResourceCollection) document;
1038            if (_canView(folder))
1039            {
1040                return _extractFolderData(folder);
1041            }
1042        }
1043        
1044        return null;
1045    }
1046    
1047    /**
1048     * Internal method to extract the valuable data of a folder
1049     * @param folder The folder
1050     * @return the valuable folder data
1051     */
1052    protected Map<String, Object> _extractFolderData(ResourceCollection folder)
1053    {
1054        Map<String, Object> data = new HashMap<>();
1055        
1056        data.put("id", folder.getId());
1057        data.put("name", folder.getName());
1058        data.put("path", _getFolderPath(folder));
1059        data.put("type", ResourceType.FOLDER.name().toLowerCase());
1060        data.put("description", StringUtils.defaultString(folder.getDescription()));
1061        
1062        AmetysObject parent = folder.getParent();
1063        if (parent != null && parent instanceof ResourceCollection)
1064        {
1065            data.put("location", ((ResourceCollection) parent).getName());
1066            data.put("parentId", parent.getId());
1067        }
1068        
1069        boolean hasChildren = _hasChildren(folder, true);
1070        if (hasChildren)
1071        {
1072            data.put("children", Collections.EMPTY_LIST);
1073        }
1074        
1075        data.put("modifiable", folder instanceof ModifiableAmetysObject);
1076        data.put("canCreateChild", folder instanceof ModifiableExplorerNode);
1077        data.put("rights", _extractFolderRightData(folder));
1078
1079        data.put("notification", false); // TODO unread notification (not yet supported)
1080        
1081        return data;
1082    }
1083    
1084    private List<String> _getFolderPath(ResourceCollection folder)
1085    {
1086        List<String> paths = new ArrayList<>();
1087        
1088        ResourceCollection rootDocuments = _getRootFromObject(folder);
1089        
1090        if (!rootDocuments.equals(folder))
1091        {
1092            List<ExplorerNode> parents = new ArrayList<>();
1093            
1094            AmetysObject parent = folder.getParent();
1095            while (parent instanceof ExplorerNode && !parent.equals(rootDocuments))
1096            {
1097                parents.add((ExplorerNode) parent);
1098                parent = parent.getParent();
1099            }
1100            
1101            parents.add(rootDocuments);
1102            
1103            Collections.reverse(parents);
1104            
1105            parents.stream().forEach(p -> 
1106            {
1107                paths.add(p.getId());
1108            });
1109        }
1110        
1111        return paths;
1112    }
1113    
1114    /**
1115     * Internal method to detect if a document has child
1116     * @param folder The folder
1117     * @param ignoreFiles Should child files be taken into account to compute the 'hasChildDocuments' data.
1118     * @return the valuable folder data
1119     */
1120    protected boolean _hasChildren(ResourceCollection folder, boolean ignoreFiles)
1121    {
1122        for (AmetysObject child : folder.getChildren())
1123        {
1124            if (child instanceof ResourceCollection && _canView((ResourceCollection) child)
1125                || !ignoreFiles && child instanceof Resource && _canView((Resource) child))
1126            {
1127                return true;
1128            }
1129        }
1130        
1131        return false;
1132    }
1133    
1134    /**
1135     * Internal method to extract the data concerning the right of the current user for a folder
1136     * @param folder The folder
1137     * @return The map of right data. Keys are the rights id, and values indicates whether the current user has the right or not.
1138     */
1139    protected  Map<String, Object> _extractFolderRightData(ResourceCollection folder)
1140    {
1141        Map<String, Object> rightsData = new HashMap<>();
1142        UserIdentity user = _currentUserProvider.getUser();
1143        
1144        // Add
1145        rightsData.put("add-file", _rightManager.hasRight(user, RIGHTS_RESOURCE_ADD, folder) == RightResult.RIGHT_ALLOW);
1146        rightsData.put("add-folder", _rightManager.hasRight(user, RIGHTS_COLLECTION_ADD, folder) == RightResult.RIGHT_ALLOW);
1147        rightsData.put("add-cmis-folder", _rightManager.hasRight(user, "Plugin_Explorer_CMIS_Add", folder) == RightResult.RIGHT_ALLOW);
1148        
1149        // Rename - Edit
1150        rightsData.put("edit", _rightManager.hasRight(user, RIGHTS_COLLECTION_EDIT, folder) == RightResult.RIGHT_ALLOW);
1151        
1152        // Delete
1153        rightsData.put("delete", _rightManager.hasRight(user, RIGHTS_COLLECTION_DELETE, folder) == RightResult.RIGHT_ALLOW);
1154        // FIXME Delete own?
1155        
1156        return rightsData;
1157    }
1158    
1159    /**
1160     * Add a file
1161     * @param part The uploaded part corresponding to the file
1162     * @param parentId Identifier of the parent collection
1163     * @param unarchive True if the file is an archive that should be unarchived (only available for ZIP file)
1164     * @param allowRename True if the file can be renamed if it already exists
1165     * @param allowUpdate True if the file can be updated if it already exists (and allowRename is false)
1166     * @return The result map with id, parentId and name keys
1167     * @throws IllegalAccessException If the user has no sufficient rights
1168     */
1169    @Callable
1170    public Map<String, Object> addFile(Part part, String parentId, boolean unarchive, boolean allowRename, boolean allowUpdate) throws IllegalAccessException
1171    {
1172        // FIXME CMS-2297
1173        // checkUserRight(object, "Plugin_Explorer_File_Add");
1174        // see AddOrUpdateResourceHelper
1175        // TODO catch IllegalAccessException -> error = has-right
1176        
1177        ModifiableResourceCollection modifiableFolder = getModifiableResourceCollection(parentId);
1178        
1179        ResourceOperationMode mode = getOperationMode(unarchive, allowRename, allowUpdate);
1180        
1181        ResourceOperationResult operationResult = _addOrUpdateResourceHelper.performResourceOperation(part, modifiableFolder, mode);
1182        
1183        // Handle result map
1184        return generateActionResult(modifiableFolder, operationResult);
1185    }
1186    /**
1187     * Add a file
1188     * @param inputStream The uploaded input stream
1189     * @param fileName desired file name
1190     * @param parentId Identifier of the parent collection
1191     * @param unarchive True if the file is an archive that should be unarchived (only available for ZIP file)
1192     * @param allowRename True if the file can be renamed if it already exists
1193     * @param allowUpdate True if the file can be updated if it already exists (and allowRename is false)
1194     * @return The result map with id, parentId and name keys
1195     * @throws IllegalAccessException If the user has no sufficient rights
1196     */
1197    public Map<String, Object> addFile(InputStream inputStream, String fileName, String parentId, boolean unarchive, boolean allowRename, boolean allowUpdate) throws IllegalAccessException
1198    {
1199     // FIXME CMS-2297
1200        // checkUserRight(object, "Plugin_Explorer_File_Add");
1201        // see AddOrUpdateResourceHelper
1202        // TODO catch IllegalAccessException -> error = has-right
1203        
1204        ModifiableResourceCollection modifiableFolder = getModifiableResourceCollection(parentId);
1205        
1206        ResourceOperationMode mode = getOperationMode(unarchive, allowRename, allowUpdate);
1207        
1208        ResourceOperationResult operationResult = _addOrUpdateResourceHelper.performResourceOperation(inputStream, fileName, modifiableFolder, mode);
1209        
1210        // Handle result map
1211        return generateActionResult(modifiableFolder, operationResult);
1212    }
1213    
1214    /**
1215     * get a {@link ModifiableResourceCollection} for an ID, or the root folder;
1216     * @param ametysId id of the resource. Can be null to get root folder.
1217     * @return ModifiableResourceCollection
1218     * @throws IllegalClassException if id links to a node which is not a {@link ModifiableResourceCollection}
1219     */
1220    private ModifiableResourceCollection getModifiableResourceCollection(String ametysId)
1221    {
1222        ResourceCollection folder = _getRootIfNull(ametysId);
1223        
1224        if (folder == null)
1225        {
1226            throw new IllegalArgumentException("Root folder not found");
1227        }
1228        
1229        if (!(folder instanceof ModifiableResourceCollection))
1230        {
1231            throw new IllegalClassException(ModifiableResourceCollection.class, folder.getClass());
1232        }
1233        
1234        return (ModifiableResourceCollection) folder;
1235    }
1236    /**
1237     * returns the {@link ResourceOperationMode} according to parameters
1238     * @param unarchive unarchive
1239     * @param allowRename allowRename
1240     * @param allowUpdate allowUpdate
1241     * @return ADD, ADD_UNZIP, ADD_RENAME, ADD_UPDATE
1242     */
1243    private ResourceOperationMode getOperationMode(boolean unarchive, boolean allowRename, boolean allowUpdate)
1244    {
1245        ResourceOperationMode mode = ResourceOperationMode.ADD;
1246        if (unarchive)
1247        {
1248            mode = ResourceOperationMode.ADD_UNZIP;
1249        }
1250        else if (allowRename)
1251        {
1252            mode = ResourceOperationMode.ADD_RENAME;
1253        }
1254        else if (allowUpdate)
1255        {
1256            mode = ResourceOperationMode.UPDATE;
1257        }
1258        return mode;
1259    }
1260    private Map<String, Object> generateActionResult(ResourceCollection folder, ResourceOperationResult operationResult)
1261    {
1262        Map<String, Object> result = new HashMap<>();
1263        
1264        if (operationResult.isSuccess())
1265        {
1266            List<Map<String, Object>> resourceData = operationResult.getResources()
1267                    .stream()
1268                    .filter(r -> r.getParent().equals(folder)) // limit to direct children
1269                    .map(this::_extractFileData)
1270                    .collect(Collectors.toList());
1271            
1272            result.put("resources", resourceData);
1273        }
1274        else
1275        {
1276            result.put("message", operationResult.getErrorMessage());
1277        }
1278        
1279        return result;
1280    }
1281    
1282    /**
1283     * Internal method to extract the valuable data of a file
1284     * @param file The file
1285     * @return the valuable file data
1286     */
1287    protected  Map<String, Object> _extractFileData(Resource file)
1288    {
1289        Map<String, Object> data = new HashMap<>();
1290        
1291        data.put("id", file.getId());
1292        data.put("name", file.getName());
1293        data.put("path", _getFilePath(file));
1294        
1295        // Encode path without extension)
1296        String resourcePath = file.getResourcePath();
1297        int i = resourcePath.lastIndexOf(".");
1298        resourcePath = i != -1 ? resourcePath.substring(0, i) : resourcePath; 
1299        // Encode twice
1300        String encodedPath = FilenameUtils.encodePath(resourcePath);
1301        data.put("encodedPath", URIUtils.encodeURI(encodedPath, Map.of()));
1302        
1303        data.put("type", ResourceType.FILE.name().toLowerCase());
1304        data.put("fileType", _workspaceHelper.getFileType(file).name().toLowerCase());
1305        data.put("fileExtension", StringUtils.substringAfterLast(file.getName(), "."));
1306        
1307        AmetysObject parent = file.getParent();
1308        if (parent != null && parent instanceof ResourceCollection)
1309        {
1310            data.put("location", ((ResourceCollection) parent).getName());
1311            data.put("parentId", parent.getId());
1312            data.put("parentPath", ((ResourceCollection) parent).getExplorerPath());
1313        }
1314        
1315        data.put("modifiable", file instanceof ModifiableResource);
1316        data.put("canCreateChild", file instanceof ModifiableExplorerNode);
1317        data.put("uri", ResolveURIComponent.resolve("project-resource", file.getId(), false, true));
1318        
1319        data.put("description", file.getDCDescription());
1320        data.put("tags", _tags2json(file));
1321        data.put("mimetype", file.getMimeType());
1322        data.put("length", String.valueOf(file.getLength()));
1323        data.putAll(_extractFileImageData(file));
1324        
1325        // TODO To remove: unused in 2.0.0
1326        UserIdentity userIdentity = file.getCreator();
1327        User user = _userManager.getUser(userIdentity.getPopulationId(), userIdentity.getLogin());
1328        String name = user == null ? userIdentity.getLogin() : user.getFullName();
1329        data.put("lastContributor", name);
1330        if (user != null)
1331        {
1332            data.put("lastContributorLogin", userIdentity.getLogin());
1333            data.put("lastContributorPopulationId", userIdentity.getPopulationId());
1334        }
1335        // End remove
1336        
1337        UserIdentity creatorIdentity = file.getCreator();
1338        data.put("creator", _userHelper.user2json(creatorIdentity));
1339        data.put("creationDate", DateUtils.dateToString(file.getCreationDate()));
1340        
1341        UserIdentity contribIdentity = file.getLastContributor();
1342        data.put("author", _userHelper.user2json(contribIdentity));
1343        data.put("lastModified", DateUtils.dateToString(file.getLastModified()));
1344        
1345        data.put("rights", _extractFileRightData(file));
1346        
1347        data.putAll(_extractFileLockData(file));
1348        
1349        return data;
1350    }
1351    
1352    /**
1353     * Get tags
1354     * @return the tags
1355     */
1356    @Callable
1357    public List<Map<String, Object>> getTags()
1358    {
1359        // FIXME get tag of project only ? get tag used on files only ?
1360        List<String> allTags = _projectManager.getTags();
1361        
1362        List<Map<String, Object>> tags = new ArrayList<>();
1363        
1364        allTags.forEach(t -> 
1365        {
1366            Map<String, Object> tag = new HashMap<>();
1367            tag.put("text", t);
1368            tag.put("color", null); // FIXME tag color is not supported yet
1369            tags.add(tag);
1370        });
1371
1372        return tags;
1373    }
1374    
1375    private List<Map<String, Object>> _tags2json(Resource file)
1376    {
1377        List<Map<String, Object>> tags = new ArrayList<>();
1378        
1379        Arrays.stream(file.getKeywords()).forEach(keyword -> 
1380        {
1381            Map<String, Object> tag = new HashMap<>();
1382            tag.put("text", keyword);
1383            tag.put("color", null); // FIXME tag color is not supported yet
1384            tags.add(tag);
1385        });
1386
1387        return tags;
1388    }
1389    
1390    private List<String> _getFilePath(Resource file)
1391    {
1392        return _getFolderPath(file.getParent());
1393    }
1394    
1395    /**
1396     * Internal method to extract image specific data of a file
1397     * @param file The file
1398     * @return The image specific data
1399     */
1400    protected  Map<String, Object> _extractFileImageData(Resource file)
1401    {
1402        Map<String, Object> imageData = new HashMap<>();
1403        
1404        BufferedImage image = _workspaceHelper.getImage(file);
1405        if (image != null)
1406        {
1407            imageData.put("imgWidth", image.getWidth());
1408            imageData.put("imgHeight", image.getHeight());
1409        }
1410        
1411        imageData.put("image", image != null);
1412        
1413        return imageData;
1414    }
1415    
1416    /**
1417     * Internal method to extract the data concerning the right of the current user for file
1418     * @param file The file
1419     * @return The map of right data. Keys are the rights id, and values indicates whether the current user has the right or not.
1420     */
1421    protected  Map<String, Object> _extractFileRightData(Resource file)
1422    {
1423        Map<String, Object> rightsData = new HashMap<>();
1424        UserIdentity user = _currentUserProvider.getUser();
1425        ResourceCollection folder = file.getParent();
1426        
1427        // Rename - Edit
1428        rightsData.put("rename", _rightManager.hasRight(user, RIGHTS_RESOURCE_RENAME, folder) == RightResult.RIGHT_ALLOW);
1429        rightsData.put("edit", _rightManager.hasRight(user, RIGHTS_RESOURCE_EDIT_DC, folder) == RightResult.RIGHT_ALLOW);
1430        
1431        // Delete
1432        rightsData.put("delete", _rightManager.hasRight(user, RIGHTS_RESOURCE_DELETE, folder) == RightResult.RIGHT_ALLOW);
1433        
1434        // TODO Delete own - no ability to detect document creator currently
1435        // rightsData.put("delete-own", ...);
1436        
1437        // Unlock
1438        rightsData.put("unlock", _rightManager.hasRight(user, RIGHTS_RESOURCE_UNLOCK_ALL, folder) == RightResult.RIGHT_ALLOW);
1439       
1440        // Comments
1441        rightsData.put("comment", _rightManager.hasRight(user, RIGHTS_RESOURCE_COMMENT, folder) == RightResult.RIGHT_ALLOW);
1442        rightsData.put("moderate-comments", _rightManager.hasRight(user, RIGHTS_RESOURCE_MODERATE_COMMENT, folder) == RightResult.RIGHT_ALLOW);
1443        
1444        return rightsData;
1445    }
1446    
1447    /**
1448     * Internal method to extract the data relative to the lock state of a file
1449     * @param file The file
1450     * @return The image specific data
1451     */
1452    protected  Map<String, Object> _extractFileLockData(Resource file)
1453    {
1454        Map<String, Object> lockData = new HashMap<>();
1455        
1456        if (file instanceof LockableAmetysObject)
1457        {
1458            boolean isLocked = ((LockableAmetysObject) file).isLocked();
1459            lockData.put("locked", isLocked);
1460            
1461            if (isLocked)
1462            {
1463                UserIdentity lockOwner = ((LockableAmetysObject) file).getLockOwner();
1464                
1465                lockData.put("isLockOwner", lockOwner.equals(_currentUserProvider.getUser()));
1466                lockData.put("lockOwner", _userHelper.user2json(lockOwner));
1467            }
1468        }
1469        
1470        return lockData;
1471    }
1472    
1473    private List<String> _sanitizeFileTags(Collection<String> tags) throws AmetysRepositoryException
1474    {
1475        // Enforce lowercase and remove possible duplicate tags
1476        return Optional.ofNullable(tags).orElseGet(ArrayList::new).stream()
1477                .map(String::trim)
1478                .map(String::toLowerCase)
1479                .distinct()
1480                .collect(Collectors.toList());
1481    }
1482    
1483    @Override
1484    protected void _setComment(JCRPost comment, String content)
1485    {
1486        try
1487        {
1488            _htmlTransformer.transform(content, comment.getContent());
1489        }
1490        catch (IOException e)
1491        {
1492            throw new AmetysRepositoryException("Failed to transform comment into rich text", e);
1493        }
1494    }
1495    
1496    @Override
1497    protected String _getComment(JCRPost post) throws AmetysRepositoryException
1498    {
1499        Source contentSource = null;
1500        try
1501        {
1502            Map<String, Object> parameters = new HashMap<>();
1503            parameters.put("source", post.getContent().getInputStream());
1504            contentSource = _sourceResolver.resolveURI("cocoon://_plugins/" + _pluginName + "/convert/html2html", null, parameters);
1505            return IOUtils.toString(contentSource.getInputStream(), "UTF-8");
1506        }
1507        catch (IOException e)
1508        {
1509            throw new AmetysRepositoryException("Failed to transform rich text into string", e);
1510        }
1511        finally
1512        {
1513            _sourceResolver.release(contentSource);
1514        }
1515    }
1516    
1517    @Override
1518    protected String _getCommentForEditing(JCRPost post) throws AmetysRepositoryException
1519    {
1520        try
1521        {
1522            StringBuilder sb = new StringBuilder();
1523            _htmlTransformer.transformForEditing(post.getContent(), sb);
1524            return sb.toString();
1525        }
1526        catch (IOException e)
1527        {
1528            throw new AmetysRepositoryException("Failed to transform rich text into string", e);
1529        }
1530    }
1531    
1532    /**
1533     * Indicates if the current user can view the folder
1534     * @param folder The folder to test
1535     * @return true if the folder can be viewed
1536     */
1537    protected boolean _canView(ResourceCollection folder)
1538    {
1539        return _rightManager.currentUserHasReadAccess(folder);
1540    }
1541    
1542    /**
1543     * Indicates if the current user can view the file
1544     * @param file The file to test
1545     * @return true if the file can be viewed 
1546     */
1547    protected boolean _canView(Resource file)
1548    {
1549        return _rightManager.currentUserHasReadAccess(file.getParent());
1550    }
1551    
1552    @Override
1553    @Callable
1554    public Map<String, String> getCMISProperties(String id)
1555    {
1556        // override to allow calls from workspaces
1557        return super.getCMISProperties(id);
1558    }
1559    
1560    @Override
1561    @Callable
1562    public Map<String, Object> addCMISCollection(String parentId, String originalName, String url, String login, String password, String repoId, boolean renameIfExists)
1563    {
1564        String rootId = parentId == null ? _getRootFromRequest().getId() : parentId;
1565        if (rootId == null)
1566        {
1567            throw new IllegalArgumentException("Unable to add CMIS collection: parent folder not found.");
1568        }
1569        return super.addCMISCollection(rootId, originalName, url, login, password, repoId, renameIfExists);
1570    }
1571    
1572    /**
1573     * Edits a CMIS folder (see {@link CMISRootResourcesCollection})
1574     * 
1575     * @param id the id of CMIS folder
1576     * @param name The name of the CMIS folder
1577     * @param url The url of CMIS repository
1578     * @param login The user's login to access CMIS repository
1579     * @param password The user's password to access CMIS repository
1580     * @param repoId The id of CMIS repository
1581     * @return the result map with id of edited node
1582     * @throws RepositoryException If an error occurred
1583     */
1584    @Callable
1585    public Map<String, Object> editCMISCollection(String id, String name, String url, String login, String password, String repoId) throws RepositoryException
1586    {
1587        List<String> errors = new LinkedList<>();
1588        try
1589        {
1590            renameObject(id, name);
1591        }
1592        catch (RepositoryException e)
1593        {
1594            getLogger().error("Repository exception during CMIS folder edition.", e);
1595            errors.add("repository");
1596        }
1597        
1598        // override to allow calls from workspaces
1599        Map<String, Object> result = super.editCMISCollection(id, url, login, password, repoId);
1600        if (errors.size() > 0)
1601        {
1602            result.put("errors", errors);
1603        }
1604        return result;
1605    }
1606    
1607    @Override
1608    @Callable
1609    public boolean isCMISCollection(String id)
1610    {
1611        // override to allow calls from workspaces
1612        return super.isCMISCollection(id);
1613    }
1614    
1615    /**
1616     * Count the total of documents in the project
1617     * @param project The project
1618     * @return The total of documents, or null if the module is not activated
1619     */
1620    public Long getDocumentsCount(Project project)
1621    {
1622        Function<Project, ResourceCollection> getModuleRoot = proj -> _moduleEP.getModule(DocumentWorkspaceModule.DODUMENT_MODULE_ID).getModuleRoot(proj, false);
1623        return Optional.ofNullable(project)
1624                .map(getModuleRoot)
1625                .map(root -> _getChildDocumentsCount(root))
1626                .orElse(null);
1627    }
1628
1629    private Long _getChildDocumentsCount(ResourceCollection collection)
1630    {
1631        return collection.getChildren().stream()
1632                // Count the number of documents : a document counts as 1, a folder count has the sum of its children. Anything else is ignored (0)
1633                .map(ao -> ao instanceof Resource ? 1L : ao instanceof ResourceCollection ? _getChildDocumentsCount((ResourceCollection) ao) : 0L)
1634                .reduce(0L, Long::sum);
1635    }
1636}