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