001/*
002 *  Copyright 2020 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.project;
017
018import java.time.ZonedDateTime;
019import java.util.ArrayList;
020import java.util.Arrays;
021import java.util.Collection;
022import java.util.Collections;
023import java.util.HashMap;
024import java.util.HashSet;
025import java.util.Iterator;
026import java.util.LinkedHashSet;
027import java.util.List;
028import java.util.Map;
029import java.util.Objects;
030import java.util.Optional;
031import java.util.Set;
032import java.util.function.Predicate;
033import java.util.regex.Pattern;
034import java.util.stream.Collectors;
035import java.util.stream.Stream;
036import java.util.stream.StreamSupport;
037
038import javax.jcr.Node;
039import javax.jcr.PathNotFoundException;
040import javax.jcr.Property;
041import javax.jcr.RepositoryException;
042import javax.jcr.Session;
043import javax.jcr.Value;
044
045import org.apache.avalon.framework.activity.Initializable;
046import org.apache.avalon.framework.component.Component;
047import org.apache.avalon.framework.context.Context;
048import org.apache.avalon.framework.context.ContextException;
049import org.apache.avalon.framework.context.Contextualizable;
050import org.apache.avalon.framework.logger.AbstractLogEnabled;
051import org.apache.avalon.framework.service.ServiceException;
052import org.apache.avalon.framework.service.ServiceManager;
053import org.apache.avalon.framework.service.Serviceable;
054import org.apache.cocoon.components.ContextHelper;
055import org.apache.cocoon.environment.Request;
056import org.apache.commons.collections.CollectionUtils;
057import org.apache.commons.lang.ArrayUtils;
058import org.apache.commons.lang3.StringUtils;
059import org.apache.commons.lang3.tuple.Pair;
060
061import org.ametys.cms.repository.ContentDAO.TagMode;
062import org.ametys.cms.tag.Tag;
063import org.ametys.core.cache.AbstractCacheManager;
064import org.ametys.core.cache.AbstractCacheManager.CacheType;
065import org.ametys.core.cache.Cache;
066import org.ametys.core.cache.CacheException;
067import org.ametys.core.group.GroupDirectoryContextHelper;
068import org.ametys.core.group.GroupIdentity;
069import org.ametys.core.observation.Event;
070import org.ametys.core.observation.ObservationManager;
071import org.ametys.core.observation.Observer;
072import org.ametys.core.right.RightManager;
073import org.ametys.core.right.RightManager.RightResult;
074import org.ametys.core.ui.Callable;
075import org.ametys.core.user.CurrentUserProvider;
076import org.ametys.core.user.UserIdentity;
077import org.ametys.core.user.population.PopulationContextHelper;
078import org.ametys.core.util.I18nUtils;
079import org.ametys.core.util.LambdaUtils;
080import org.ametys.plugins.core.impl.cache.AbstractCacheKey;
081import org.ametys.plugins.core.search.UserAndGroupSearchManager;
082import org.ametys.plugins.core.user.UserHelper;
083import org.ametys.plugins.explorer.ExplorerNode;
084import org.ametys.plugins.explorer.resources.ModifiableResourceCollection;
085import org.ametys.plugins.repository.AmetysObject;
086import org.ametys.plugins.repository.AmetysObjectIterable;
087import org.ametys.plugins.repository.AmetysObjectResolver;
088import org.ametys.plugins.repository.AmetysRepositoryException;
089import org.ametys.plugins.repository.CollectionIterable;
090import org.ametys.plugins.repository.ModifiableAmetysObject;
091import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
092import org.ametys.plugins.repository.RepositoryConstants;
093import org.ametys.plugins.repository.UnknownAmetysObjectException;
094import org.ametys.plugins.repository.jcr.JCRAmetysObject;
095import org.ametys.plugins.repository.jcr.NodeTypeHelper;
096import org.ametys.plugins.repository.provider.AbstractRepository;
097import org.ametys.plugins.repository.provider.JackrabbitRepository;
098import org.ametys.plugins.repository.provider.WorkspaceSelector;
099import org.ametys.plugins.repository.query.expression.Expression;
100import org.ametys.plugins.repository.query.expression.Expression.Operator;
101import org.ametys.plugins.repository.query.expression.StringExpression;
102import org.ametys.plugins.workspaces.ObservationConstants;
103import org.ametys.plugins.workspaces.catalog.CatalogSiteType;
104import org.ametys.plugins.workspaces.categories.Category;
105import org.ametys.plugins.workspaces.categories.CategoryProviderExtensionPoint;
106import org.ametys.plugins.workspaces.members.JCRProjectMember;
107import org.ametys.plugins.workspaces.members.JCRProjectMember.MemberType;
108import org.ametys.plugins.workspaces.members.ProjectMemberManager;
109import org.ametys.plugins.workspaces.members.ProjectMemberManager.ProjectMember;
110import org.ametys.plugins.workspaces.project.modules.WorkspaceModule;
111import org.ametys.plugins.workspaces.project.modules.WorkspaceModuleExtensionPoint;
112import org.ametys.plugins.workspaces.project.objects.Project;
113import org.ametys.plugins.workspaces.project.objects.Project.InscriptionStatus;
114import org.ametys.plugins.workspaces.project.rights.ProjectRightHelper;
115import org.ametys.plugins.workspaces.tags.ProjectTagProviderExtensionPoint;
116import org.ametys.plugins.workspaces.util.StatisticColumn;
117import org.ametys.plugins.workspaces.util.StatisticsColumnType;
118import org.ametys.runtime.config.Config;
119import org.ametys.runtime.i18n.I18nizableText;
120import org.ametys.runtime.plugin.component.PluginAware;
121import org.ametys.web.repository.page.ModifiablePage;
122import org.ametys.web.repository.page.Page;
123import org.ametys.web.repository.page.PageQueryHelper;
124import org.ametys.web.repository.page.SitemapElement;
125import org.ametys.web.repository.site.Site;
126import org.ametys.web.repository.site.SiteDAO;
127import org.ametys.web.repository.site.SiteManager;
128import org.ametys.web.repository.sitemap.Sitemap;
129import org.ametys.web.site.SiteConfigurationManager;
130
131/**
132 * Helper component for managing project workspaces
133 */
134public class ProjectManager extends AbstractLogEnabled implements Serviceable, Component, Contextualizable, PluginAware, Initializable, Observer
135{
136    /** Avalon Role */
137    public static final String ROLE = ProjectManager.class.getName();
138    
139    /** Constant for the {@link Cache} id (the {@link Cache} is in {@link CacheType#REQUEST REQUEST} attribute) for the {@link Project}s objects
140     *  in cache by {@link RequestProjectCacheKey} (composition of project name and workspace name). */
141    public static final String REQUEST_PROJECTBYID_CACHE = ProjectManager.class.getName() + "$ProjectById";
142    
143    /** Constant for the {@link Cache} id for the {@link Project} ids (as {@link String}s) in cache by project name (for whole application). */
144    public static final String MEMORY_PROJECTIDBYNAMECACHE = ProjectManager.class.getName() + "$UUID";
145
146    /** Constant for the {@link Cache} id (the {@link Cache} is in {@link CacheType#REQUEST REQUEST} attribute) for the {@link Page}s objects
147     *  in cache by {@link RequestModuleCacheKey} (composition of project name and module name). */
148    public static final String REQUEST_PAGESBYPROJECTANDMODULE_CACHE = ProjectManager.class.getName() + "$PagesByModule";
149    
150    /** Constant for the {@link Cache} id for the {@link Project} ids (as {@link String}s) in cache by project name (for whole application). */
151    public static final String MEMORY_PAGESBYIDCACHE = ProjectManager.class.getName() + "$PageUUID";
152
153    /** Constant for the {@link Cache} id for the {@link Project} ids (as {@link String}s) in cache by site name (for whole application). */
154    public static final String MEMORY_SITEASSOCIATION_CACHE = ProjectManager.class.getName() + "$SiteAssociation";
155
156    /** Workspaces plugin node name */
157    private static final String __WORKSPACES_PLUGIN_NODE_NAME = "workspaces";
158    
159    /** Workspaces plugin node name */
160    private static final String __WORKSPACES_PLUGIN_NODE_TYPE = RepositoryConstants.NAMESPACE_PREFIX + ":unstructured";
161    
162    /** The name of the projects root node */
163    private static final String __PROJECTS_ROOT_NODE_NAME = "projects";
164    
165    /** The type of the projects root node */
166    private static final String __PROJECTS_ROOT_NODE_TYPE = RepositoryConstants.NAMESPACE_PREFIX + ":unstructured";
167    
168    /** Constants for tags metadata */
169    private static final String __PROJECTS_TAGS_PROPERTY = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":tags";
170    
171    /** Constants for places metadata */
172    private static final String __PROJECTS_PLACES_PROPERTY = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":places";
173
174    private static final String __PAGE_MODULES_VALUE = "workspaces-modules";
175
176    private static final String __IS_CACHE_FILLED = "###iscachefilled###";
177
178    /** Ametys object resolver */
179    protected AmetysObjectResolver _resolver;
180    
181    /** The i18n utils. */
182    protected I18nUtils _i18nUtils;
183    
184    /** Site manager */
185    protected SiteManager _siteManager;
186    
187    /** Site DAO */
188    protected SiteDAO _siteDao;
189    
190    /** Site configuration manager */
191    protected SiteConfigurationManager _siteConfigurationManager;
192    
193    /** Module Managers EP */
194    protected WorkspaceModuleExtensionPoint _moduleManagerEP;
195
196    /** Helper for user population */
197    protected PopulationContextHelper _populationContextHelper;
198    
199    /** Helper for group directory's context */
200    protected GroupDirectoryContextHelper _groupDirectoryContextHelper;
201    
202    /** The project members' manager */
203    protected ProjectMemberManager _projectMemberManager;
204
205    /** Avalon context */
206    protected Context _context;
207    
208    private ObservationManager _observationManager;
209    
210    private CurrentUserProvider _currentUserProvider;
211
212    private ProjectMemberManager _projectMembers;
213
214    private String _pluginName;
215
216    private ProjectRightHelper _projectRightHelper;
217
218    private ProjectTagProviderExtensionPoint _projectTagProviderEP;
219
220    private CategoryProviderExtensionPoint _categoryProviderEP;
221    
222    private UserHelper _userHelper;
223
224    private AbstractCacheManager _cacheManager;
225
226    private UserAndGroupSearchManager _userAndGroupSearchManager;
227
228    private JackrabbitRepository _repository;
229
230    private WorkspaceSelector _workspaceSelector;
231
232    private RightManager _rightManager;
233
234    private GroupDirectoryContextHelper _directoryContextHelper;
235
236    @Override
237    public void contextualize(Context context) throws ContextException
238    {
239        _context = context;
240    }
241    
242    @Override
243    public void service(ServiceManager manager) throws ServiceException
244    {
245        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
246        _repository = (JackrabbitRepository) manager.lookup(AbstractRepository.ROLE);
247        _workspaceSelector = (WorkspaceSelector) manager.lookup(WorkspaceSelector.ROLE);
248        _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE);
249        _siteManager = (SiteManager) manager.lookup(SiteManager.ROLE);
250        _siteDao = (SiteDAO) manager.lookup(SiteDAO.ROLE);
251        _siteConfigurationManager = (SiteConfigurationManager) manager.lookup(SiteConfigurationManager.ROLE);
252        _projectMembers = (ProjectMemberManager) manager.lookup(ProjectMemberManager.ROLE);
253        _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE);
254        _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
255        _moduleManagerEP = (WorkspaceModuleExtensionPoint) manager.lookup(WorkspaceModuleExtensionPoint.ROLE);
256        _projectRightHelper = (ProjectRightHelper) manager.lookup(ProjectRightHelper.ROLE);
257        _projectTagProviderEP = (ProjectTagProviderExtensionPoint) manager.lookup(ProjectTagProviderExtensionPoint.ROLE);
258        _userHelper = (UserHelper) manager.lookup(UserHelper.ROLE);
259        _categoryProviderEP = (CategoryProviderExtensionPoint) manager.lookup(CategoryProviderExtensionPoint.ROLE);
260        _cacheManager = (AbstractCacheManager) manager.lookup(AbstractCacheManager.ROLE);
261        _populationContextHelper = (PopulationContextHelper) manager.lookup(PopulationContextHelper.ROLE);
262        _groupDirectoryContextHelper = (GroupDirectoryContextHelper) manager.lookup(GroupDirectoryContextHelper.ROLE);
263        _projectMemberManager = (ProjectMemberManager) manager.lookup(ProjectMemberManager.ROLE);
264        _userAndGroupSearchManager = (UserAndGroupSearchManager) manager.lookup(UserAndGroupSearchManager.ROLE);
265        _rightManager = (RightManager) manager.lookup(RightManager.ROLE);
266        _directoryContextHelper = (GroupDirectoryContextHelper) manager.lookup(GroupDirectoryContextHelper.ROLE);
267    }
268    
269    public void initialize() throws Exception
270    {
271        _createCaches();
272        _observationManager.registerObserver(this);
273    }
274    
275    @Override
276    public void setPluginInfo(String pluginName, String featureName, String id)
277    {
278        _pluginName = pluginName;
279    }
280
281    /**
282     * Enumeration for the profile to assign for new module
283     */
284    public enum ProfileForNewModule
285    {
286        /** No profile assigned for new modules */
287        NONE,
288        
289        /** Default profile assigned for new modules */
290        DEFAULT_MEMBER_PROFILE
291        
292    }
293    
294    /**
295     * Retrieves all projects
296     * @return the projects
297     */
298    public AmetysObjectIterable<Project> getProjects()
299    {
300        return getProjects(true);
301    }
302    
303    /**
304     * Retrieves all projects
305     * @param onlyWorking true to retrieve only working projects with non null sites
306     * @return the projects
307     */
308    public AmetysObjectIterable<Project> getProjects(boolean onlyWorking)
309    {
310        // As cache is computed from default JCR workspace, we need to filter on sites that exist into the current JCR workspace
311        Set<Project> projects = _getUUIDCache().values().stream()
312                .filter(_resolver::hasAmetysObjectForId)
313                .map(_resolver::<Project>resolveById)
314                // If needed, check if the site is not null, as it can happen if site was deleted from admin side
315                .filter(project -> !onlyWorking || Objects.nonNull(project.getSite()))
316                .collect(Collectors.toSet());
317        
318        return new CollectionIterable<>(projects);
319    }
320    
321    /**
322     * Retrieves projects filtered by categories
323     * @param filteredCategories the filtered categories. Can be empty to no filter by categories.
324     * @return the projects
325     */
326    public List<Project> getProjects(Set<String> filteredCategories)
327    {
328        return getProjects(filteredCategories, null, null, true);
329    }
330    
331    /**
332     * Retrieves projects filtered by categories and/or keywords
333     * @param filteredCategories the filtered categories. Can be empty to no filter by categories.
334     * @param filteredKeywords the filtered keywords. Can be empty to no filter by keywords.
335     * @param anyMatch true to get projects matching categories OR keywords OR pattern
336     * @return the projects
337     */
338    public List<Project> getProjects(Set<String> filteredCategories, Set<String> filteredKeywords, boolean anyMatch)
339    {
340        return getProjects(filteredCategories, filteredKeywords, null, anyMatch);
341    }
342    
343    /**
344     * Retrieves projects filtered by categories and/or keywords and/or pattern
345     * @param filteredCategories the filtered categories. Can be empty to no filter by categories.
346     * @param filteredKeywords the filtered keywords. Can be empty to no filter by keywords.
347     * @param pattern to filter on pattern. Can be null or empty to no filter on pattern
348     * @param anyMatch true to get projects matching categories OR keywords OR pattern
349     * @return the projects
350     */
351    public List<Project> getProjects(Set<String> filteredCategories, Set<String> filteredKeywords, String pattern, boolean anyMatch)
352    {
353        return getProjects(filteredCategories, filteredKeywords, null, anyMatch, false);
354    }
355    
356    /**
357     * Retrieves projects filtered by categories and/or keywords and/or pattern
358     * @param filteredCategories the filtered categories. Can be empty to no filter by categories.
359     * @param filteredKeywords the filtered keywords. Can be empty to no filter by keywords.
360     * @param pattern to filter on pattern. Can be null or empty to no filter on pattern
361     * @param anyMatch true to get projects matching categories OR keywords OR pattern
362     * @param excludePrivate true to exclude private projects
363     * @return the projects
364     */
365    public List<Project> getProjects(Set<String> filteredCategories, Set<String> filteredKeywords, String pattern, boolean anyMatch, boolean excludePrivate)
366    {
367        Predicate<Project> matchCategories = p -> filteredCategories == null || filteredCategories.isEmpty() || !Collections.disjoint(p.getCategories(), filteredCategories);
368        Predicate<Project> matchKeywords = p -> filteredKeywords == null || filteredKeywords.isEmpty() || !Collections.disjoint(Arrays.asList(p.getKeywords()), filteredKeywords);
369        
370        Pattern patternFilter = StringUtils.isNotEmpty(pattern) ? Pattern.compile(pattern, Pattern.CASE_INSENSITIVE) : null;
371        Predicate<Project> matchPattern = p -> patternFilter == null
372                || p.getTitle() != null && patternFilter.matcher(p.getTitle()).find()
373                || p.getDescription() != null && patternFilter.matcher(p.getDescription()).find();
374        
375        Predicate<Project> fullMatch = null;
376        if (anyMatch)
377        {
378            fullMatch = matchCategories.or(matchKeywords).or(matchPattern);
379        }
380        else
381        {
382            fullMatch = matchCategories.and(matchKeywords).and(matchPattern);
383        }
384        
385        Predicate<Project> matchStatus = p ->  !excludePrivate || p.getInscriptionStatus() != InscriptionStatus.PRIVATE;
386        
387        return getProjects()
388                .stream()
389                .filter(matchStatus)
390                .filter(fullMatch)
391                .collect(Collectors.toList());
392    }
393    
394    /**
395     * Get the projects categories
396     * @return the projects categories
397     */
398    public Set<Category> getProjectsCategories()
399    {
400        return getProjects().stream()
401            .map(Project::getCategories)
402            .flatMap(Collection::stream)
403            .map(id -> _categoryProviderEP.getTag(id, null))
404            .filter(Objects::nonNull)
405            .collect(Collectors.toSet());
406    }
407    
408    /**
409     * Get the user's projects categories
410     * @param user the user
411     * @return the user's projects categories
412     */
413    public Set<Category> getUserProjectsCategories(UserIdentity user)
414    {
415        return getUserProjects(user).keySet()
416                .stream()
417                .map(Project::getCategories)
418                .flatMap(Collection::stream)
419                .map(id -> _categoryProviderEP.getTag(id, null))
420                .filter(Objects::nonNull)
421                .collect(Collectors.toSet());
422    }
423    
424    /**
425     * Get the projects modules
426     * @return the projects modules
427     */
428    public Set<WorkspaceModule> getProjectsModules()
429    {
430        return getProjects().stream()
431            .map(Project::getModules)
432            .flatMap(Arrays::stream)
433            .map(id -> _moduleManagerEP.<WorkspaceModule>getModule(id))
434            .filter(Objects::nonNull)
435            .collect(Collectors.toSet());
436    }
437    
438    /**
439     * Get the projects modules
440     * @param user the user
441     * @return the projects modules
442     */
443    public Set<WorkspaceModule> getUserProjectsModules(UserIdentity user)
444    {
445        return getUserProjects(user).keySet()
446            .stream()
447            .map(Project::getModules)
448            .flatMap(Arrays::stream)
449            .map(id -> _moduleManagerEP.<WorkspaceModule>getModule(id))
450            .filter(Objects::nonNull)
451            .collect(Collectors.toSet());
452    }
453    
454    /**
455     * Retrieves all projects for client side
456     * @return the projects
457     */
458    @Callable(rights = "Runtime_Rights_Admin_Access", context = "/admin")
459    public List<Map<String, Object>> getProjectsForClientSide()
460    {
461        return getProjects(false)
462                .stream()
463                .map(p -> getProjectProperties(p))
464                .collect(Collectors.toList());
465    }
466    
467    
468    /**
469     * Retrieves a project by its name
470     * @param projectName The project name
471     * @return the project or <code>null</code> if not found
472     */
473    public Project getProject(String projectName)
474    {
475        if (StringUtils.isBlank(projectName))
476        {
477            return null;
478        }
479        
480        Request request = _getRequest();
481        if (request == null)
482        {
483            // There is no request to store cache
484            return _computeProject(projectName);
485        }
486        
487        Cache<RequestProjectCacheKey, Project> projectsCache = _getRequestProjectCache();
488        
489        // The site key in the cache is of the form {site + workspace}.
490        String currentWorkspace = _workspaceSelector.getWorkspace();
491        RequestProjectCacheKey projectKey = RequestProjectCacheKey.of(projectName, currentWorkspace);
492        
493        try
494        {
495            Project project = projectsCache.get(projectKey, __ -> _computeProject(projectName));
496            return project;
497        }
498        catch (CacheException e)
499        {
500            if (e.getCause() instanceof UnknownAmetysObjectException)
501            {
502                throw new UnknownAmetysObjectException(e.getMessage());
503            }
504            else
505            {
506                throw e;
507            }
508        }
509    }
510    
511    /**
512     * Get the list of existing projects that current user is a member of.
513     * @return a map of the projects
514     */
515    @Callable
516    public Map<String, Object> getUserProjects2JSON()
517    {
518        Map<String, Object> result = new HashMap<>();
519                
520        UserIdentity user = _currentUserProvider.getUser();
521        
522        List<Map<String, Object>> projects = new ArrayList<>();
523        Map<Project, MemberType> userProjects = getUserProjects(user);
524
525        projects.addAll(userProjects.keySet()
526                .stream()
527                .map(p -> getProjectProperties(p))
528                .collect(Collectors.toList()));
529        
530        result.put("projects", projects);
531        return result;
532    }
533    
534    /**
535     * Get the user's projects
536     * @param user the user
537     * @return the user's projects
538     */
539    public Map<Project, MemberType> getUserProjects(UserIdentity user)
540    {
541        return getUserProjects(user, Set.of());
542    }
543    
544    /**
545     * Get the user's projects filtered by categories
546     * @param user the user
547     * @param filteredCategories the filtered categories. Can be empty to no filter by categories
548     * @return the user's projects
549     */
550    public Map<Project, MemberType> getUserProjects(UserIdentity user, Set<String> filteredCategories)
551    {
552        return getUserProjects(user, filteredCategories, null, null, true);
553    }
554    
555    /**
556     * Get the user's projects filtered by categories OR keywords
557     * @param user the user
558     * @param filteredCategories the filtered categories. Can be empty to no filter by categories
559     * @param filteredKeywords the filtered keywords. Can be empty to no filter by keywords
560     * @return the user's projects
561     */
562    public Map<Project, MemberType> getUserProjects(UserIdentity user, Set<String> filteredCategories, Set<String> filteredKeywords)
563    {
564        return getUserProjects(user, filteredCategories, filteredKeywords, null, true);
565    }
566    
567    /**
568     * Get the user's projects filtered by categories, keywords and/or pattern
569     * @param user the user
570     * @param filteredCategories the filtered categories. Can be empty to no filter by categories.
571     * @param filteredKeywords the filtered keywords. Can be empty to no filter by keywords.
572     * @param pattern to filter on pattern. Can be null or empty to no filter on pattern
573     * @param anyMatch true to get projects matching categories OR keywords OR pattern
574     * @return the user's projects
575     */
576    public Map<Project, MemberType> getUserProjects(UserIdentity user, Set<String> filteredCategories, Set<String> filteredKeywords, String pattern, boolean anyMatch)
577    {
578        return getUserProjects(user, filteredCategories, filteredKeywords, pattern, true, false);
579    }
580    
581    /**
582     * Get the user's projects filtered by categories, keywords and/or pattern
583     * @param user the user
584     * @param filteredCategories the filtered categories. Can be empty to no filter by categories.
585     * @param filteredKeywords the filtered keywords. Can be empty to no filter by keywords.
586     * @param pattern to filter on pattern. Can be null or empty to no filter on pattern
587     * @param anyMatch true to get projects matching categories OR keywords OR pattern
588     * @param excludePrivate true to exclude private projects
589     * @return the user's projects
590     */
591    public Map<Project, MemberType> getUserProjects(UserIdentity user, Set<String> filteredCategories, Set<String> filteredKeywords, String pattern, boolean anyMatch, boolean excludePrivate)
592    {
593        Map<Project, MemberType> userProjects = new HashMap<>();
594        
595        List<Project> projects = getProjects(filteredCategories, filteredKeywords, pattern, anyMatch, excludePrivate);
596        for (Project project : projects)
597        {
598            ProjectMember member = _projectMembers.getProjectMember(project, user);
599            if (member != null)
600            {
601                userProjects.put(project, member.getType());
602            }
603        }
604        
605        return userProjects;
606    }
607
608    /**
609     * Get the projects managed by the user
610     * @param user the user
611     * @return the projects for which the user is a manager
612     */
613    public List<Project> getManagedProjects(UserIdentity user)
614    {
615        return getManagedProjects(user, Set.of());
616    }
617    
618    /**
619     * Get the projects managed by the user
620     * @param user the user
621     * @param filteredCategories the filtered categories. Can be empty to no filter by categories.
622     * @return the projects for which the user is a manager
623     */
624    public List<Project> getManagedProjects(UserIdentity user, Set<String> filteredCategories)
625    {
626        return getProjects(filteredCategories)
627            .stream()
628            .filter(p -> ArrayUtils.contains(p.getManagers(), user))
629            .collect(Collectors.toList());
630    }
631    
632    /**
633     * Returns true if the given project exists.
634     * @param projectName the project name.
635     * @return true if the given project exists.
636     */
637    public boolean hasProject(String projectName)
638    {
639        Map<String, String> uuidCache = _getUUIDCache();
640        if (uuidCache.containsKey(projectName))
641        {
642            // As cache is computed from default JCR workspace, we need to check if the project exists into the current JCR workspace
643            return _resolver.hasAmetysObjectForId(uuidCache.get(projectName));
644        }
645        return false;
646    }
647    
648    /**
649     * Get all managers
650     * @return the managers
651     */
652    public Set<UserIdentity> getManagers()
653    {
654        return getProjects()
655            .stream()
656            .map(Project::getManagers)
657            .flatMap(Arrays::stream)
658            .collect(Collectors.toSet());
659    }
660    
661    /**
662     * Determines if the current user is a manager of at least one project
663     * @return true if the user is a manager
664     */
665    public boolean isManager()
666    {
667        return isManager(_currentUserProvider.getUser());
668    }
669    
670    /**
671     * Determines if the user is a manager of at least one project
672     * @param user the user
673     * @return true if the user is a manager
674     */
675    public boolean isManager(UserIdentity user)
676    {
677        AmetysObjectIterable<Project> projects = getProjects();
678        for (Project project : projects)
679        {
680            if (isManager(project, user))
681            {
682                return true;
683            }
684        }
685        return false;
686    }
687    
688    /**
689     * Determines if the user is a manager of the project
690     * @param projectName the project name
691     * @param user the user
692     * @return true if the user is a manager
693     */
694    public boolean isManager(String projectName, UserIdentity user)
695    {
696        Project project = getProject(projectName);
697        if (project != null)
698        {
699            return isManager(project, user);
700        }
701        return false;
702    }
703    
704    /**
705     * Determines if the user is a manager of the project
706     * @param project the project
707     * @param user the user
708     * @return true if the user is a manager
709     */
710    public boolean isManager(Project project, UserIdentity user)
711    {
712        return ArrayUtils.contains(project.getManagers(), user);
713    }
714
715    /**
716     * Can the current user access backoffice on the site of the current project
717     * @param project The non null project to analyse
718     * @return true if the user can access to the backoffice
719     */
720    public boolean canAccessBO(Project project)
721    {
722        Site site = project.getSite();
723        if (site == null)
724        {
725            return false;
726        }
727
728        Request request = ContextHelper.getRequest(_context);
729        String currentSiteName = (String) request.getAttribute("siteName");
730        try
731        {
732            request.setAttribute("siteName", site.getName()); // Setting temporarily this attribute to check user rights on any object on this site
733            return !_rightManager.getUserRights(_currentUserProvider.getUser(), "/cms").isEmpty();
734        }
735        finally
736        {
737            request.setAttribute("siteName", currentSiteName);
738        }
739    }
740    
741    /**
742     * Can the current user leave the project
743     * @param project The non null project to analyse
744     * @return true if the user can leave the project
745     */
746    public boolean canLeaveProject(Project project)
747    {
748        if (project == null)
749        {
750            return false;
751        }
752        
753        Site site = project.getSite();
754        if (site == null)
755        {
756            return false;
757        }
758        
759        UserIdentity userIdentity = _currentUserProvider.getUser();
760        
761        // As a user in a group can't leave the project, check if he is registered as User member
762        Set<ProjectMember> members = _projectMemberManager.getProjectMembers(project, false);
763        ProjectMember projectMember = members.stream()
764                .filter(member -> MemberType.USER == member.getType())
765                .filter(member -> userIdentity.equals(member.getUser().getIdentity()))
766                .findFirst()
767                .orElse(null);
768        
769        // The user is either on a group, or has not been found
770        if (projectMember == null)
771        {
772            return false;
773        }
774
775        // The user is the only remaining manager, and therefore can not leave the project
776        if (_projectMemberManager.isOnlyManager(project, userIdentity))
777        {
778            return false;
779        }
780        
781        return true;
782    }
783    
784    /**
785     * Retrieves the mapping of all the projects name with their title on which the current user has access
786     * @return the map (projectName, projectTitle) for all projects
787     */
788    @Callable
789    public List<Map<String, Object>> getUserProjectsData()
790    {
791        return getUserProjects(_currentUserProvider.getUser())
792                .keySet()
793                .stream()
794                .map(p -> _project2json(p))
795                .collect(Collectors.toList());
796    }
797
798    /**
799     * Retrieves the users that have not been yet added to a project with a given criteria
800     * @param projectName the project name
801     * @param limit limit of request
802     * @param criteria the criteria of the search
803     * @param previousSearchData the previous search data to compute offset. Null if first search
804     * @return list of users
805     */
806    @SuppressWarnings("unchecked")
807    @Callable
808    public Map<String, Object> searchUserByProject(String projectName, int limit, String criteria, Map<String, Object> previousSearchData)
809    {
810
811        Map<String, Object> results = new HashMap<>();
812        Project project = this.getProject(projectName);
813        Site site = project.getSite();
814        
815        Set<String> projectMemberList = _projectMemberManager.getProjectMembers(project, false)
816                .stream()
817                .map(member ->
818                {
819                    if (member.getType() == MemberType.USER)
820                    {
821                        return UserIdentity.userIdentityToString(member.getUser().getIdentity());
822                    }
823                    else
824                    {
825                        GroupIdentity groupIdentityAsString = member.getGroup().getIdentity();
826                        return GroupIdentity.groupIdentityToString(groupIdentityAsString);
827                    }
828                })
829                .collect(Collectors.toSet());
830        
831        Set<String> contexts = new HashSet<>(Arrays.asList("/sites/" + site.getName(), "/sites-fo/" + site.getName()));
832        
833        Map<String, Object> params = new HashMap<>();
834        params.put("pattern", criteria);
835        
836        Map<String, Object> result = new HashMap<>();
837        Map<String, Object> searchData = previousSearchData;
838        List<Map<String, Object>> memberList = new ArrayList<>();
839
840        Set<String> groupDirectories = _directoryContextHelper.getGroupDirectoriesOnContexts(contexts);
841        Set<String> userPopulations = getPopulation(site, true);
842
843        do
844        {
845            result = _userAndGroupSearchManager.searchUsersAndGroup(userPopulations, groupDirectories, limit - memberList.size(), searchData, params, true);
846            List<Map<String, Object>> filteredMembers = ((List<Map<String, Object>>) result.get("results"))
847                    .stream()
848                    .filter(member ->
849                    {
850                        return !projectMemberList.contains(member.get("login") + "#" + member.get("populationId")) && !projectMemberList.contains(member.get("id") + "#" + member.get("groupDirectory"));
851                    }).collect(Collectors.toList());
852            searchData = (Map<String, Object>) result.get("searchData");
853            memberList.addAll(filteredMembers);
854        }
855        while (!result.containsKey("finished") && memberList.size() < limit);
856
857        results.put("searchData", searchData);
858        results.put("memberList", memberList);
859        return results;
860    }
861    
862    /**
863     * Get the populations of the project
864     * @param site the project site
865     * @param excludeConfigurationPopulations true to exclude populations configured in the catalog site or in the project site
866     * @return the populations of the project
867     */
868    public Set<String> getPopulation(Site site, boolean excludeConfigurationPopulations)
869    {
870        Set<String> contexts = new HashSet<>(Arrays.asList("/sites/" + site.getName(), "/sites-fo/" + site.getName()));
871
872        Set<String> userPopulations = _populationContextHelper.getUserPopulationsOnContexts(contexts, false, true);
873        
874        if (excludeConfigurationPopulations)
875        {
876            Site catalogSite = _siteManager.getSite(getCatalogSiteName());
877            
878            // Get the excluded population configuration
879            String excludedPopulationsString = catalogSite.getValue(CatalogSiteType.PROJECT_EXCLUDED_POPULATIONS_SITE_PARAM);
880            
881            // Check if project site overrides catalog configuration
882            if (site.getValue(ProjectWorkspaceSiteType.PROJECT_OVERRIDE_EXCLUDED_POPULATIONS_SITE_PARAM, false, false))
883            {
884                excludedPopulationsString = site.getValue(ProjectWorkspaceSiteType.PROJECT_EXCLUDED_POPULATIONS_PROJECT_SITE_PARAM);
885            }
886            
887            List<String> excludedPopulationsList = Arrays.asList(StringUtils.split(StringUtils.defaultString(excludedPopulationsString), ","));
888            
889            if (!excludedPopulationsList.isEmpty())
890            {
891                userPopulations.removeAll(excludedPopulationsList);
892            }
893        }
894
895        return userPopulations;
896    }
897
898    /**
899     * Retrieves the mapping of all the projects name with their title (regarless user rights)
900     * @return the map (projectName, projectTitle) for all projects
901     */
902    @Callable(rights = "Runtime_Rights_Admin_Access", context = "/admin")
903    public List<Map<String, Object>> getProjectsData()
904    {
905        return getProjects()
906                .stream()
907                .map(p -> _project2json(p))
908                .collect(Collectors.toList());
909    }
910    
911    /**
912     * Get the project's main properties as json object
913     * @param project the project
914     * @return the json representation of project
915     */
916    protected Map<String, Object> _project2json(Project project)
917    {
918        Map<String, Object> json = new HashMap<>();
919        
920        json.put("id", project.getId());
921        json.put("name", project.getName());
922        json.put("title", project.getTitle());
923        json.put("url", getProjectUrl(project, StringUtils.EMPTY));
924        
925        return json;
926    }
927    /**
928     * Retrieves the project names
929     * @return the project names
930     */
931    @Callable(rights = "Runtime_Rights_Admin_Access", context = "/admin")
932    public Collection<String> getProjectNames()
933    {
934        // As cache is computed from default JCR workspace, we need to filter on sites that exist into the current JCR workspace
935        return _getUUIDCache().entrySet().stream()
936                .filter(e -> _resolver.hasAmetysObjectForId(e.getValue()))
937                .map(Map.Entry::getKey)
938                .collect(Collectors.toList());
939    }
940    
941    /**
942     * Return the root for projects
943     * The root node will be created if necessary
944     * @return The root for projects
945     */
946    public ModifiableTraversableAmetysObject getProjectsRoot()
947    {
948        try
949        {
950            ModifiableTraversableAmetysObject pluginsNode = _resolver.resolveByPath("/ametys:plugins");
951            ModifiableTraversableAmetysObject workspacesPluginNode = _getOrCreateObject(pluginsNode, __WORKSPACES_PLUGIN_NODE_NAME, __WORKSPACES_PLUGIN_NODE_TYPE);
952            return _getOrCreateObject(workspacesPluginNode, __PROJECTS_ROOT_NODE_NAME, __PROJECTS_ROOT_NODE_TYPE);
953        }
954        catch (AmetysRepositoryException e)
955        {
956            throw new AmetysRepositoryException("Error getting the projects root node.", e);
957        }
958    }
959    
960    /**
961     * Retrieves the standard information of a project
962     * @param projectId Identifier of the project
963     * @return The map of information
964     */
965    @Callable(rights = "Runtime_Rights_Admin_Access", context = "/admin")
966    public Map<String, Object> getProjectProperties(String projectId)
967    {
968        return getProjectProperties((Project) _resolver.resolveById(projectId));
969    }
970    
971    /**
972     * Retrieves the standard information of a project
973     * @param project The project
974     * @return The map of information
975     */
976    public Map<String, Object> getProjectProperties(Project project)
977    {
978        Map<String, Object> info = new HashMap<>();
979
980        info.put("id", project.getId());
981        info.put("name", project.getName());
982        info.put("type", "project");
983
984        info.put("title", project.getTitle());
985        info.put("description", project.getDescription());
986        info.put("inscriptionStatus", project.getInscriptionStatus().toString());
987        info.put("defaultProfile", project.getDefaultProfile());
988
989        info.put("creationDate", project.getCreationDate());
990
991        // check if the project workspace configuration is valid
992        Site site = project.getSite();
993        boolean valid = site != null && _siteConfigurationManager.isSiteConfigurationValid(site);
994        
995        Set<String> categories = project.getCategories();
996        info.put("categories", categories.stream()
997                .map(c -> _categoryProviderEP.getTag(c, new HashMap<>()))
998                .filter(Objects::nonNull)
999                .map(t -> _tag2json(t))
1000                .collect(Collectors.toList()));
1001        
1002        Set<String> tags = project.getTags();
1003        info.put("tags", tags.stream()
1004                .map(c -> _projectTagProviderEP.getTag(c, new HashMap<>()))
1005                .filter(Objects::nonNull)
1006                .map(t -> _tag2json(t))
1007                .collect(Collectors.toList()));
1008        
1009        info.put("valid", valid);
1010        
1011        UserIdentity[] managers = project.getManagers();
1012        info.put("managers", Arrays.stream(managers)
1013                .map(u -> _userHelper.user2json(u))
1014                .collect(Collectors.toList()));
1015        
1016        Map<String, String> siteProps = new HashMap<>();
1017        // site map with id ,name, title and url property
1018        // { id: site id, name: site name, title: site title, url: site url }
1019        if (site != null)
1020        {
1021            siteProps.put("id", site.getId());
1022            siteProps.put("name", site.getName());
1023            siteProps.put("title", site.getTitle());
1024            siteProps.put("url", site.getUrl());
1025        }
1026        info.put("site", siteProps);
1027
1028        return info;
1029    }
1030    
1031    private Map<String, Object> _tag2json(Tag tag)
1032    {
1033        Map<String, Object> json = new HashMap<>();
1034        json.put("id", tag.getId());
1035        json.put("name", tag.getName());
1036        json.put("title", tag.getTitle());
1037        return json;
1038    }
1039
1040    /**
1041     * Get the project URL.
1042     * @param project The project
1043     * @param defaultValue The default value to use if there is no site
1044     * @return The project URL if a site is configured, otherwise return the default value.
1045     */
1046    public String getProjectUrl(Project project, String defaultValue)
1047    {
1048        Site site = project.getSite();
1049        if (site == null)
1050        {
1051            return defaultValue;
1052        }
1053        else
1054        {
1055            return site.getUrl();
1056        }
1057    }
1058    
1059    /**
1060     * Create a project
1061     * @param name The project name
1062     * @param title The project title
1063     * @param description The project description
1064     * @param emailList Project mailing list
1065     * @param inscriptionStatus The inscription status of the project
1066     * @param defaultProfile The default profile for new members
1067     * @return A map containing the id of the new project or an error key.
1068     */
1069    @Callable
1070    public Map<String, Object> createProject(String name, String title, String description, String emailList, String inscriptionStatus, String defaultProfile)
1071    {
1072        checkRightsForProjectCreation(InscriptionStatus.valueOf(inscriptionStatus.toUpperCase()), null);
1073
1074        Map<String, Object> result = new HashMap<>();
1075        List<String> errors = new ArrayList<>();
1076        
1077        Map<String, Object> additionalValues = new HashMap<>();
1078        additionalValues.put("description", description);
1079        additionalValues.put("emailList", emailList);
1080        additionalValues.put("inscriptionStatus", inscriptionStatus);
1081        additionalValues.put("defaultProfile", defaultProfile);
1082        
1083        Project project = createProject(name, title, additionalValues, null, errors);
1084        
1085        if (CollectionUtils.isEmpty(errors))
1086        {
1087            result.put("id", project.getId());
1088        }
1089        else
1090        {
1091            result.put("error", errors.get(0));
1092        }
1093        
1094        return result;
1095    }
1096    
1097    /**
1098     * Create a project
1099     * @param name The project name
1100     * @param title The project title
1101     * @param additionalValues A list of optional additional values. Accepted values are : description, mailingList, inscriptionStatus, defaultProfile, tags, categoryTags, keywords and language
1102     * @param modulesIds The list of modules to activate. Can be null to activate all modules
1103     * @param errors A list that will be populated with the encountered errors. If null, errors will not be tracked.
1104     * @return The id of the new project
1105     */
1106    public Project createProject(String name, String title, Map<String, Object> additionalValues, Set<String> modulesIds, List<String> errors)
1107    {
1108        if (StringUtils.isEmpty(title))
1109        {
1110            throw new IllegalArgumentException(String.format("Cannot create project. Title is mandatory"));
1111        }
1112        
1113        ModifiableTraversableAmetysObject projectsRoot = getProjectsRoot();
1114        
1115        // Project name should be unique
1116        if (hasProject(name))
1117        {
1118            if (getLogger().isWarnEnabled())
1119            {
1120                getLogger().warn(String.format("A project with the name '%s' already exists", name));
1121            }
1122            
1123            if (errors != null)
1124            {
1125                errors.add("project-exists");
1126            }
1127            
1128            return null;
1129        }
1130        
1131        Project project = projectsRoot.createChild(name, Project.NODE_TYPE);
1132        project.setTitle(title);
1133        String description = (String) additionalValues.getOrDefault("description", null);
1134        if (StringUtils.isNotEmpty(description))
1135        {
1136            project.setDescription(description);
1137        }
1138        String mailingList = (String) additionalValues.getOrDefault("emailList", null);
1139        if (StringUtils.isNotEmpty(mailingList))
1140        {
1141            project.setMailingList(mailingList);
1142        }
1143        
1144        String inscriptionStatus = (String) additionalValues.getOrDefault("inscriptionStatus", null);
1145        if (StringUtils.isNotEmpty(inscriptionStatus))
1146        {
1147            project.setInscriptionStatus(inscriptionStatus);
1148        }
1149        
1150        String defaultProfile = (String) additionalValues.getOrDefault("defaultProfile", null);
1151        if (StringUtils.isNotEmpty(defaultProfile))
1152        {
1153            project.setDefaultProfile(defaultProfile);
1154        }
1155        
1156        @SuppressWarnings("unchecked")
1157        List<String> tags = (List<String>) additionalValues.getOrDefault("tags", null);
1158        if (tags != null)
1159        {
1160            project.setTags(tags);
1161        }
1162        @SuppressWarnings("unchecked")
1163        List<String> categoryTags = (List<String>) additionalValues.getOrDefault("categoryTags", null);
1164        if (categoryTags != null)
1165        {
1166            project.setCategoryTags(categoryTags);
1167        }
1168        
1169        @SuppressWarnings("unchecked")
1170        List<String> keywords = (List<String>) additionalValues.getOrDefault("keywords", null);
1171        if (keywords != null)
1172        {
1173            project.setKeywords(keywords.toArray(new String[keywords.size()]));
1174        }
1175
1176        project.setCreationDate(ZonedDateTime.now());
1177        
1178        // Create the project workspace = a site + a set of pages
1179        _createProjectWorkspace(project, errors);
1180        
1181        activateModules(project, modulesIds, additionalValues);
1182        
1183        if (CollectionUtils.isEmpty(errors))
1184        {
1185            project.saveChanges();
1186         
1187            // Notify observers
1188            Map<String, Object> eventParams = new HashMap<>();
1189            eventParams.put(ObservationConstants.ARGS_PROJECT, project);
1190            _observationManager.notify(new Event(ObservationConstants.EVENT_PROJECT_ADDED, _currentUserProvider.getUser(), eventParams));
1191            
1192        }
1193        else
1194        {
1195            deleteProject(project);
1196        }
1197        
1198        clearCaches();
1199        
1200        return project;
1201    }
1202    
1203    /**
1204     * Edit a project
1205     * @param id The project identifier
1206     * @param title The title to set
1207     * @param description The description to set
1208     * @param mailingList Project mailing list
1209     * @param inscriptionStatus The inscription status of the project
1210     * @param defaultProfile The default profile for new members
1211     */
1212    @Callable(rights = ProjectConstants.RIGHT_PROJECT_EDIT, context = "/admin")
1213    public void editProject(String id, String title, String description, String mailingList, String inscriptionStatus, String defaultProfile)
1214    {
1215        Project project = _resolver.resolveById(id);
1216        editProject(project, title, description, mailingList, inscriptionStatus, defaultProfile);
1217    }
1218    
1219    /**
1220     * Edit a project
1221     * @param project The project
1222     * @param title The title to set
1223     * @param description The description to set
1224     * @param mailingList Project mailing list
1225     * @param inscriptionStatus The inscription status of the project
1226     * @param defaultProfile The default profile for new members
1227     */
1228    public void editProject(Project project, String title, String description, String mailingList, String inscriptionStatus, String defaultProfile)
1229    {
1230        checkRightsForProjectEdition(project, InscriptionStatus.valueOf(inscriptionStatus.toUpperCase()), null);
1231        
1232        project.setTitle(title);
1233        
1234        if (StringUtils.isNotEmpty(description))
1235        {
1236            project.setDescription(description);
1237        }
1238        else
1239        {
1240            project.removeDescription();
1241        }
1242        
1243        if (StringUtils.isNotEmpty(mailingList))
1244        {
1245            project.setMailingList(mailingList);
1246        }
1247        else
1248        {
1249            project.removeMailingList();
1250        }
1251
1252        project.setInscriptionStatus(inscriptionStatus);
1253        project.setDefaultProfile(defaultProfile);
1254        
1255        project.saveChanges();
1256        
1257        // Notify observers
1258        Map<String, Object> eventParams = new HashMap<>();
1259        eventParams.put(ObservationConstants.ARGS_PROJECT, project);
1260        _observationManager.notify(new Event(ObservationConstants.EVENT_PROJECT_UPDATED, _currentUserProvider.getUser(), eventParams));
1261    }
1262    
1263    /**
1264     * Delete a list of project.
1265     * @param ids The ids of projects to delete
1266     * @return The ids of the deleted projects, unknowns projects and the deleted sites
1267     */
1268    @Callable(rights = ProjectConstants.RIGHT_PROJECT_DELETE, context = "/admin")
1269    public Map<String, Object> deleteProjectsByIds(List<String> ids)
1270    {
1271        Map<String, Object> result = new HashMap<>();
1272        List<Map<String, Object>> deleted = new ArrayList<>();
1273        List<String> unknowns = new ArrayList<>();
1274        
1275        for (String id : ids)
1276        {
1277            try
1278            {
1279                Project project = _resolver.resolveById(id);
1280                
1281                Map<String, Object> projectInfo = new HashMap<>();
1282                projectInfo.put("id", id);
1283                projectInfo.put("title", project.getTitle());
1284                projectInfo.put("sites", deleteProject(project));
1285                
1286                deleted.add(projectInfo);
1287            }
1288            catch (UnknownAmetysObjectException e)
1289            {
1290                getLogger().warn(String.format("Unable to delete the definition of id '%s', because it does not exist.", id), e);
1291                unknowns.add(id);
1292            }
1293        }
1294        
1295        result.put("deleted", deleted);
1296        result.put("unknowns", unknowns);
1297        
1298        return result;
1299    }
1300    
1301    /**
1302     * Delete a project.
1303     * @param projects The list of projects to delete
1304     * @return list of deleted sites (each list entry contains a data map with
1305     *         the id and the name of the delete site).
1306     */
1307    public List<Map<String, String>> deleteProject(List<Project> projects)
1308    {
1309        List<Map<String, String>> deletedSitesInfo = new ArrayList<>();
1310        
1311        for (Project project : projects)
1312        {
1313            deletedSitesInfo.addAll(deleteProject(project));
1314        }
1315        
1316        return deletedSitesInfo;
1317    }
1318    
1319    /**
1320     * Delete a project and its sites
1321     * @param project The project to delete
1322     * @return list of deleted sites (each list entry contains a data map with
1323     *         the id and the name of the delete site).
1324     */
1325    public List<Map<String, String>> deleteProject(Project project)
1326    {
1327        ModifiableAmetysObject parent = project.getParent();
1328        
1329        
1330        // list of map entry with id, name and title property
1331        // { id: site id, name: site name }
1332        List<Map<String, String>> deletedSitesInfo = new ArrayList<>();
1333        
1334        Site site = project.getSite();
1335        if (site != null)
1336        {
1337            try
1338            {
1339                Map<String, String> siteProps = new HashMap<>();
1340                siteProps.put("id", site.getId());
1341                siteProps.put("name", site.getName());
1342                
1343                _siteDao.deleteSite(site.getId());
1344                deletedSitesInfo.add(siteProps);
1345            }
1346            catch (RepositoryException e)
1347            {
1348                String errorMsg = String.format("Error while trying to delete the site '%s' for the project '%s'.", site.getName(), project.getName());
1349                getLogger().error(errorMsg, e);
1350            }
1351        }
1352        
1353        String projectId = project.getId();
1354        Collection<ProjectMember> projectMembers = _projectMembers.getProjectMembers(project, true);
1355        project.remove();
1356        parent.saveChanges();
1357        
1358        // Notify observers
1359        Map<String, Object> eventParams = new HashMap<>();
1360        eventParams.put(ObservationConstants.ARGS_PROJECT_ID, projectId);
1361        eventParams.put(ObservationConstants.ARGS_PROJECT_NAME, project.getName());
1362        eventParams.put(ObservationConstants.ARGS_PROJECT_MEMBERS, projectMembers);
1363        _observationManager.notify(new Event(ObservationConstants.EVENT_PROJECT_DELETED, _currentUserProvider.getUser(), eventParams));
1364        
1365        clearCaches();
1366        
1367        return deletedSitesInfo;
1368    }
1369    
1370    /**
1371     * Utility method to get or create an ametys object
1372     * @param <A> A sub class of AmetysObject
1373     * @param parent The parent object
1374     * @param name The ametys object name
1375     * @param type The ametys object type
1376     * @return ametys object
1377     * @throws AmetysRepositoryException if an repository error occurs
1378     */
1379    private <A extends AmetysObject> A _getOrCreateObject(ModifiableTraversableAmetysObject parent, String name, String type) throws AmetysRepositoryException
1380    {
1381        A object;
1382        
1383        if (parent.hasChild(name))
1384        {
1385            object = parent.getChild(name);
1386        }
1387        else
1388        {
1389            object = parent.createChild(name, type);
1390            parent.saveChanges();
1391        }
1392        
1393        return object;
1394    }
1395    
1396    /**
1397     * Get the project of an ametys object inside a project.
1398     * It can be an explorer node, or any type of resource in a module.
1399     * @param id The identifier of the ametys object
1400     * @return the project or null if not found
1401     */
1402    public Project getParentProject(String id)
1403    {
1404        return getParentProject(_resolver.<AmetysObject>resolveById(id));
1405    }
1406    
1407    /**
1408     * Get the project of an ametys object inside a project.
1409     * It can be an explorer node, or any type of resource in a module.
1410     * @param object The ametys object
1411     * @return the project or null if not found
1412     */
1413    public Project getParentProject(AmetysObject object)
1414    {
1415        AmetysObject ametysObject = object;
1416        // Go back to the local explorer root.
1417        do
1418        {
1419            ametysObject = ametysObject.getParent();
1420        }
1421        while (ametysObject instanceof ExplorerNode);
1422        
1423        if (!(ametysObject instanceof Project))
1424        {
1425            getLogger().warn(String.format("No project found for ametys object with id '%s'", ametysObject.getId()));
1426            return null;
1427        }
1428        
1429        return (Project) ametysObject;
1430    }
1431    
1432    /**
1433     * Get the list of project names for a given site
1434     * @param siteName The site name
1435     * @return the list of project names
1436     */
1437    @Callable(rights = "Runtime_Rights_Admin_Access", context = "/admin")
1438    public List<String> getProjectsForSite(String siteName)
1439    {
1440        Cache<String, List<Pair<String, String>>> cache = _getMemorySiteAssociationCache();
1441        if (cache.hasKey(siteName))
1442        {
1443            return cache.get(siteName).stream()
1444                    .map(p -> p.getRight())
1445                    .collect(Collectors.toList());
1446        }
1447        else
1448        {
1449            List<String> projectNames = new ArrayList<>();
1450            
1451            if (_siteManager.hasSite(siteName))
1452            {
1453                Site site = _siteManager.getSite(siteName);
1454                getProjectsForSite(site)
1455                    .stream()
1456                    .map(Project::getName)
1457                    .forEach(projectNames::add);
1458            }
1459            
1460            return projectNames;
1461        }
1462    }
1463    
1464    /**
1465     * Get the list of project for a given site
1466     * @param site The site
1467     * @return the list of project
1468     */
1469    public List<Project> getProjectsForSite(Site site)
1470    {
1471        Cache<String, List<Pair<String, String>>> cache = _getMemorySiteAssociationCache();
1472        if (cache.hasKey(site.getName()))
1473        {
1474            return cache.get(site.getName()).stream()
1475                        .map(p -> _resolver.<Project>resolveById(p.getLeft()))
1476                        .collect(Collectors.toList());
1477        }
1478        
1479        try
1480        {
1481            // Stream over the weak reference properties pointing to this
1482            // node to find referencing projects
1483            Iterator<Property> propertyIterator = site.getNode().getWeakReferences();
1484            Iterable<Property> propertyIterable = () -> propertyIterator;
1485            
1486            List<Project> projects = StreamSupport.stream(propertyIterable.spliterator(), false)
1487                    .map(p ->
1488                    {
1489                        try
1490                        {
1491                            Node parent = p.getParent();
1492
1493                            // Check if the parent is a project"
1494                            if (NodeTypeHelper.isNodeType(parent, "ametys:project"))
1495                            {
1496                                Project project = _resolver.resolve(parent, false);
1497                                return project;
1498                            }
1499                        }
1500                        catch (Exception e)
1501                        {
1502                            if (getLogger().isWarnEnabled())
1503                            {
1504                                // this weak reference is not from a project
1505                                String propertyPath = null;
1506                                try
1507                                {
1508                                    propertyPath = p.getPath();
1509                                }
1510                                catch (Exception e2)
1511                                {
1512                                    // ignore
1513                                }
1514                                
1515                                String warnMsg = String.format("Site '%s' is pointed by a weak reference '%s' which is not representing a relation with project. This reference is ignored.", site.getName(), propertyPath);
1516                                getLogger().warn(warnMsg);
1517                            }
1518                        }
1519                        
1520                        return null;
1521                    })
1522                    .filter(Objects::nonNull)
1523                    .collect(Collectors.toList());
1524            
1525            List<Pair<String, String>> projectsPairs = projects.stream().map(p -> Pair.of(p.getId(), p.getName())).collect(Collectors.toList());
1526            cache.put(site.getName(), projectsPairs);
1527            return projects;
1528        }
1529        catch (RepositoryException e)
1530        {
1531            getLogger().error(String.format("Unable to find projects for site '%s'", site.getName()), e);
1532        }
1533        
1534        return new ArrayList<>();
1535    }
1536    
1537    /**
1538     * Create the project workspace for a given project.
1539     * @param project The project for which the workspace must be created
1540     * @param errors A list of possible errors to populate. Can be null if the caller is not interested in error tracking.
1541     * @return The site created for this workspace
1542     */
1543    protected Site _createProjectWorkspace(Project project, List<String> errors)
1544    {
1545        String initialSiteName = project.getName();
1546        Site site = null;
1547        
1548        Site catalogSite = _siteManager.getSite(getCatalogSiteName());
1549        String rootId = catalogSite != null ? catalogSite.getId() : null;
1550        
1551        Map<String, Object> result = _siteDao.createSite(rootId, initialSiteName, ProjectWorkspaceSiteType.TYPE_ID, true);
1552        
1553        String siteId = (String) result.get("id");
1554        String siteName = (String) result.get("name");
1555        if (StringUtils.isNotEmpty(siteId))
1556        {
1557            // Creation success
1558            site = _siteManager.getSite(siteName);
1559            
1560            setProjectSiteTitle(site, project.getTitle());
1561          
1562            // Add site to project
1563            project.setSite(site);
1564            
1565            site.saveChanges();
1566        }
1567        
1568        return site;
1569    }
1570    
1571    /**
1572     * Get the project's tags
1573     * @return The project's tags
1574     */
1575    @Callable
1576    public List<String> getTags()
1577    {
1578        AmetysObject projectsRootNode = getProjectsRoot();
1579        if (projectsRootNode instanceof JCRAmetysObject)
1580        {
1581            Node node = ((JCRAmetysObject) projectsRootNode).getNode();
1582            
1583            try
1584            {
1585                return Arrays.stream(node.getProperty(__PROJECTS_TAGS_PROPERTY).getValues())
1586                    .map(LambdaUtils.wrap(Value::getString))
1587                    .collect(Collectors.toList());
1588            }
1589            catch (PathNotFoundException e)
1590            {
1591                // property is not set, empty list will be returned.
1592            }
1593            catch (RepositoryException e)
1594            {
1595                throw new AmetysRepositoryException(e);
1596            }
1597        }
1598        
1599        return new ArrayList<>();
1600    }
1601    
1602    /**
1603     * Set the tags
1604     * @param tags The tags to set
1605     */
1606    @Callable
1607    public synchronized void setTags(List<String> tags)
1608    {
1609        AmetysObject projectsRootNode = getProjectsRoot();
1610        if (projectsRootNode instanceof JCRAmetysObject)
1611        {
1612            JCRAmetysObject jcrProjectsRootNode = (JCRAmetysObject) projectsRootNode;
1613            
1614            if (CollectionUtils.isNotEmpty(tags))
1615            {
1616                String[] tagsArray = tags.stream()
1617                        .map(String::trim)
1618                        .map(String::toLowerCase)
1619                        .filter(StringUtils::isNotEmpty)
1620                        .distinct()
1621                        .toArray(String[]::new);
1622                
1623                try
1624                {
1625                    jcrProjectsRootNode.getNode().setProperty(__PROJECTS_TAGS_PROPERTY, tagsArray);
1626                    jcrProjectsRootNode.saveChanges();
1627                }
1628                catch (RepositoryException e)
1629                {
1630                    throw new AmetysRepositoryException(e);
1631                }
1632            }
1633            else
1634            {
1635                Node node = jcrProjectsRootNode.getNode();
1636                try
1637                {
1638                    if (node.hasProperty(__PROJECTS_TAGS_PROPERTY))
1639                    {
1640                        node.getProperty(__PROJECTS_TAGS_PROPERTY).remove();
1641                        jcrProjectsRootNode.saveChanges();
1642                    }
1643                }
1644                catch (RepositoryException e)
1645                {
1646                    throw new AmetysRepositoryException(e);
1647                }
1648            }
1649        }
1650    }
1651    
1652    /**
1653     * Add project's tags
1654     * @param newTags The new tags to add
1655     */
1656    @Callable
1657    public synchronized void addTags(Collection<String> newTags)
1658    {
1659        if (CollectionUtils.isNotEmpty(newTags))
1660        {
1661            AmetysObject projectsRootNode = getProjectsRoot();
1662            if (projectsRootNode instanceof JCRAmetysObject)
1663            {
1664                // Concat existing tags with new lowercased tags
1665                String[] tags = Stream.concat(getTags().stream(), newTags.stream().map(String::trim).map(String::toLowerCase).filter(StringUtils::isNotEmpty))
1666                        .distinct()
1667                        .toArray(String[]::new);
1668                
1669                try
1670                {
1671                    ((JCRAmetysObject) projectsRootNode).getNode().setProperty(__PROJECTS_TAGS_PROPERTY, tags);
1672                }
1673                catch (RepositoryException e)
1674                {
1675                    throw new AmetysRepositoryException(e);
1676                }
1677            }
1678        }
1679    }
1680    
1681    /**
1682     * Get the project's places
1683     * @return The project's places
1684     */
1685    @Callable
1686    public List<String> getPlaces()
1687    {
1688        AmetysObject projectsRootNode = getProjectsRoot();
1689        if (projectsRootNode instanceof JCRAmetysObject)
1690        {
1691            Node node = ((JCRAmetysObject) projectsRootNode).getNode();
1692            
1693            try
1694            {
1695                return Arrays.stream(node.getProperty(__PROJECTS_PLACES_PROPERTY).getValues())
1696                    .map(LambdaUtils.wrap(Value::getString))
1697                    .collect(Collectors.toList());
1698            }
1699            catch (PathNotFoundException e)
1700            {
1701                // property is not set, empty list will be returned.
1702            }
1703            catch (RepositoryException e)
1704            {
1705                throw new AmetysRepositoryException(e);
1706            }
1707        }
1708        
1709        return new ArrayList<>();
1710    }
1711    
1712    /**
1713     * Add project's places
1714     * @param newPlaces The new places to add
1715     */
1716    public synchronized void addPlaces(Collection<String> newPlaces)
1717    {
1718        if (CollectionUtils.isNotEmpty(newPlaces))
1719        {
1720            AmetysObject projectsRootNode = getProjectsRoot();
1721            if (projectsRootNode instanceof JCRAmetysObject)
1722            {
1723                Set<String> lowercasedPlaces = new HashSet<>();
1724                
1725                // Concat existing places with new places
1726                String[] places = Stream.concat(getPlaces().stream(), newPlaces.stream().map(String::trim).filter(StringUtils::isNotEmpty))
1727                        // duplicates are filtered out
1728                        .filter(p -> lowercasedPlaces.add(p.toLowerCase()))
1729                        .toArray(String[]::new);
1730                
1731                try
1732                {
1733                    ((JCRAmetysObject) projectsRootNode).getNode().setProperty(__PROJECTS_PLACES_PROPERTY, places);
1734                }
1735                catch (RepositoryException e)
1736                {
1737                    throw new AmetysRepositoryException(e);
1738                }
1739            }
1740        }
1741    }
1742    
1743    /**
1744     * Set the places
1745     * @param places The places to set
1746     */
1747    @Callable
1748    public synchronized void setPlaces(List<String> places)
1749    {
1750        AmetysObject projectsRootNode = getProjectsRoot();
1751        if (projectsRootNode instanceof JCRAmetysObject)
1752        {
1753            JCRAmetysObject jcrProjectsRootNode = (JCRAmetysObject) projectsRootNode;
1754            
1755            if (CollectionUtils.isNotEmpty(places))
1756            {
1757                Set<String> lowercasedPlaces = new HashSet<>();
1758                
1759                String[] placesArray = places.stream()
1760                        .map(String::trim)
1761                        .filter(StringUtils::isNotEmpty)
1762                        // duplicates are filtered out
1763                        .filter(p -> lowercasedPlaces.add(p.toLowerCase()))
1764                        .toArray(String[]::new);
1765                
1766                try
1767                {
1768                    jcrProjectsRootNode.getNode().setProperty(__PROJECTS_PLACES_PROPERTY, placesArray);
1769                    jcrProjectsRootNode.saveChanges();
1770                }
1771                catch (RepositoryException e)
1772                {
1773                    throw new AmetysRepositoryException(e);
1774                }
1775            }
1776            else
1777            {
1778                Node node = jcrProjectsRootNode.getNode();
1779                try
1780                {
1781                    if (node.hasProperty(__PROJECTS_PLACES_PROPERTY))
1782                    {
1783                        node.getProperty(__PROJECTS_PLACES_PROPERTY).remove();
1784                        jcrProjectsRootNode.saveChanges();
1785                    }
1786                }
1787                catch (RepositoryException e)
1788                {
1789                    throw new AmetysRepositoryException(e);
1790                }
1791            }
1792        }
1793    }
1794    
1795    /**
1796     * Get the list of activated modules for a project
1797     * @param project The project
1798     * @return The list of activated modules
1799     */
1800    public List<WorkspaceModule> getModules(Project project)
1801    {
1802        return _moduleManagerEP.getModules().stream()
1803                .filter(module -> isModuleActivated(project, module.getId()))
1804                .collect(Collectors.toList());
1805    }
1806    
1807    /**
1808     * Retrieves the page of the module for all available languages
1809     * @param project The project
1810     * @param moduleId The project module id
1811     * @return the page or null if not found
1812     */
1813    public Set<Page> getModulePages(Project project, String moduleId)
1814    {
1815        if (_moduleManagerEP.hasExtension(moduleId))
1816        {
1817            WorkspaceModule module = _moduleManagerEP.getExtension(moduleId);
1818            return getModulePages(project, module);
1819        }
1820        return null;
1821    }
1822    
1823    /**
1824     * Return the possible module roots associated to a page
1825     * @param page The given page
1826     * @return A non null set of the data of the linked modules
1827     */
1828    public Set<ModifiableResourceCollection> pageToModuleRoot(Page page)
1829    {
1830        Set<ModifiableResourceCollection> data = new LinkedHashSet<>();
1831        
1832        Page rootPage = page;
1833        SitemapElement parent = page.getParent();
1834        while (!(parent instanceof Sitemap) && !page.hasValue(__PAGE_MODULES_VALUE))
1835        {
1836            rootPage = (Page) parent;
1837            parent = parent.getParent();
1838        }
1839        
1840        String[] modulesRootsIds = rootPage.getValue(__PAGE_MODULES_VALUE, new String[0]);
1841        if (modulesRootsIds.length > 0)
1842        {
1843            for (String moduleRootId : modulesRootsIds)
1844            {
1845                try
1846                {
1847                    ModifiableResourceCollection moduleRoot = _resolver.resolveById(moduleRootId);
1848                    data.add(moduleRoot);
1849                }
1850                catch (UnknownAmetysObjectException e)
1851                {
1852                    // Ignore obsolete data
1853                }
1854            }
1855        }
1856        
1857        return data;
1858    }
1859    
1860    /**
1861     * Mark the given page as this module page. The modified page will not be saved.
1862     * @param page The page to change
1863     * @param moduleRoot The workspace module that use this page
1864     */
1865    public void tagProjectPage(ModifiablePage page, ModifiableResourceCollection moduleRoot)
1866    {
1867        String[] currentModules = page.getValue(__PAGE_MODULES_VALUE, new String[0]);
1868        
1869        Set<String> modules = new LinkedHashSet<>(Arrays.asList(currentModules));
1870        modules.add(moduleRoot.getId());
1871        
1872        String[] newModules = new String[modules.size()];
1873        modules.toArray(newModules);
1874        
1875        page.setValue(__PAGE_MODULES_VALUE, newModules);
1876    }
1877
1878    /**
1879     * Remove the mark on the given page of this module. The modified page will not be saved.
1880     * @param page The page to change
1881     * @param moduleRoot The workspace module that use this page
1882     */
1883    public void untagProjectPage(ModifiablePage page, ModifiableResourceCollection moduleRoot)
1884    {
1885        if (moduleRoot != null)
1886        {
1887            String[] currentModules = page.getValue(__PAGE_MODULES_VALUE, new String[0]);
1888            
1889            Set<String> modules = new LinkedHashSet<>(Arrays.asList(currentModules));
1890            modules.remove(moduleRoot.getId());
1891            
1892            String[] newModules = new String[modules.size()];
1893            modules.toArray(newModules);
1894            
1895            page.setValue(__PAGE_MODULES_VALUE, newModules);
1896        }
1897    }
1898
1899    /**
1900     * Get a page in the site of a given project with a specific tag
1901     * @param project The project
1902     * @param workspaceModule the module
1903     * @return The module's pages
1904     */
1905    public Set<Page> getModulePages(Project project, WorkspaceModule workspaceModule)
1906    {
1907        Request request = _getRequest();
1908        if (request == null)
1909        {
1910            // There is no request to store cache
1911            return _computePages(project, workspaceModule);
1912        }
1913        
1914        Cache<RequestModuleCacheKey, Set<Page>> pagesCache = _getRequestPageCache();
1915        
1916        // The site key in the cache is of the form {site + workspace}.
1917        String currentWorkspace = _workspaceSelector.getWorkspace();
1918        RequestModuleCacheKey pagesKey = RequestModuleCacheKey.of(project.getName(), workspaceModule.getId(), currentWorkspace);
1919        
1920        try
1921        {
1922            return pagesCache.get(pagesKey, __ -> _computePages(project, workspaceModule));
1923        }
1924        catch (CacheException e)
1925        {
1926            if (e.getCause() instanceof UnknownAmetysObjectException)
1927            {
1928                throw (UnknownAmetysObjectException) e.getCause();
1929            }
1930            else
1931            {
1932                throw new RuntimeException("An error occurred while computing page of module " + workspaceModule.getModuleName() + " in project " + project.getName(), e);
1933            }
1934        }
1935    }
1936    
1937    private Set<Page> _computePages(Project project, WorkspaceModule workspaceModule)
1938    {
1939        Set<String> pagesUUids = _getMemoryPageCache().get(ModuleCacheKey.of(project.getName(), workspaceModule.getId()), __ -> _computePagesIds(project, workspaceModule));
1940        if (pagesUUids != null)
1941        {
1942            return pagesUUids.stream().map(uuid -> _resolver.<Page>resolveById(uuid)).collect(Collectors.toSet());
1943        }
1944        else
1945        {
1946            // Project may be present in cache for 'default' workspace but does not exist in current JCR workspace
1947            throw new UnknownAmetysObjectException("There is no pages for '" + project.getName() + "', module '" + workspaceModule.getModuleName() + "'");
1948        }
1949    }
1950    
1951    private Set<String> _computePagesIds(Project project, WorkspaceModule workspaceModule)
1952    {
1953        Site site = project.getSite();
1954        String siteName = site != null ? site.getName() : null;
1955        if (StringUtils.isEmpty(siteName))
1956        {
1957            return null;
1958        }
1959        
1960        ModifiableResourceCollection moduleRoot = workspaceModule.getModuleRoot(project, false);
1961        if (moduleRoot != null)
1962        {
1963            Expression expression = new StringExpression(__PAGE_MODULES_VALUE, Operator.EQ, moduleRoot.getId());
1964            String query = PageQueryHelper.getPageXPathQuery(siteName, null, null, expression, null);
1965    
1966            return StreamSupport.stream(_resolver.query(query).spliterator(), false)
1967                        .map(page -> page.getId())
1968                        .collect(Collectors.toSet());
1969        }
1970        else
1971        {
1972            return Set.of();
1973        }
1974    }
1975
1976    /**
1977     * Activate the list of module of the project
1978     * @param project The project
1979     * @param moduleIds The list of modules. Can be null to activate all modules
1980     * @param additionalValues A list of optional additional values. Accepted values are : description, mailingList, inscriptionStatus, defaultProfile, tags, categoryTags, keywords and language
1981     */
1982    public void activateModules(Project project, Set<String> moduleIds, Map<String, Object> additionalValues)
1983    {
1984        Set<String> modules = moduleIds == null ? _moduleManagerEP.getExtensionsIds() : moduleIds;
1985        
1986        for (String moduleId : modules)
1987        {
1988            WorkspaceModule module = _moduleManagerEP.getModule(moduleId);
1989            if (module != null && !isModuleActivated(project, moduleId))
1990            {
1991                module.activateModule(project, additionalValues);
1992                project.addModule(moduleId);
1993            }
1994        }
1995        
1996        _setDefaultProfileForMembers(project, modules);
1997        
1998        project.saveChanges();
1999    }
2000
2001    private void _setDefaultProfileForMembers(Project project, Set<String> modules)
2002    {
2003        String profileForNewModule = StringUtils.defaultString(Config.getInstance().getValue("workspaces.profile.new.module"));
2004        
2005        if (profileForNewModule.equals(ProfileForNewModule.DEFAULT_MEMBER_PROFILE.name()))
2006        {
2007            Set<String> defaultProfiles = Set.of(StringUtils.defaultString(Config.getInstance().getValue("workspaces.profile.default")));
2008            Map<JCRProjectMember, Object> projectMembers = _projectMembers.getJCRProjectMembers(project);
2009            
2010            for (String moduleId : modules)
2011            {
2012                WorkspaceModule module = _moduleManagerEP.getModule(moduleId);
2013                for (JCRProjectMember member : projectMembers.keySet())
2014                {
2015                    
2016                    _projectMembers.setProfileOnModule(member, project, module, defaultProfiles);
2017                }
2018            }
2019        }
2020    }
2021    
2022    /**
2023     * Initialize the sitemap with the active module of the project
2024     * @param project The project
2025     * @param sitemap The sitemap
2026     */
2027    public void initializeModulesSitemap(Project project, Sitemap sitemap)
2028    {
2029        Set<String> modules = _moduleManagerEP.getExtensionsIds();
2030        
2031        for (String moduleId : modules)
2032        {
2033            if (_moduleManagerEP.hasExtension(moduleId))
2034            {
2035                WorkspaceModule module = _moduleManagerEP.getExtension(moduleId);
2036                
2037                if (isModuleActivated(project, moduleId))
2038                {
2039                    module.initializeSitemap(project, sitemap);
2040                }
2041            }
2042        }
2043    }
2044    
2045    /**
2046     * Determines if a module is activated
2047     * @param project The project
2048     * @param moduleId The id of module
2049     * @return true if the module the currently activated
2050     */
2051    public boolean isModuleActivated(Project project, String moduleId)
2052    {
2053        return ArrayUtils.contains(project.getModules(), moduleId);
2054    }
2055    
2056    /**
2057     * Remove the explorer root node of the project module, remove all events
2058     * related to that module and set it to deactivated
2059     * @param project The project
2060     * @param moduleIds The id of module to activate
2061     */
2062    public void deactivateModules(Project project, Set<String> moduleIds)
2063    {
2064        for (String moduleId : moduleIds)
2065        {
2066            WorkspaceModule module = _moduleManagerEP.getModule(moduleId);
2067            if (module != null && isModuleActivated(project, moduleId))
2068            {
2069                module.deactivateModule(project);
2070                project.removeModule(moduleId);
2071            }
2072        }
2073        
2074        project.saveChanges();
2075    }
2076    
2077    
2078    /**
2079     * Get the list of profiles configured for the workspaces' projects
2080     * @return The list of profiles as JSON
2081     */
2082    @Callable
2083    public Map<String, Object> getProjectProfiles()
2084    {
2085        Map<String, Object> result = new HashMap<>();
2086        List<Map<String, Object>> profiles = _projectRightHelper.getProfiles().stream().map(p -> p.toJSON()).collect(Collectors.toList());
2087        result.put("profiles", profiles);
2088        return result;
2089    }
2090    
2091    /**
2092     * Get the tags from the projects
2093     * @param projectIds The ids of the projects
2094     * @return the tags of the projects
2095     */
2096    @Callable
2097    public Set<String> getTags(List<String> projectIds)
2098    {
2099        Set<String> tags = new HashSet<>();
2100        
2101        for (String projectId : projectIds)
2102        {
2103            Project project = _resolver.resolveById(projectId);
2104            tags.addAll(project.getTags());
2105        }
2106        
2107        return tags;
2108    }
2109    
2110    /**
2111     * Tag the projects
2112     * @param projectIds the project ids
2113     * @param tagNames the tag names
2114     * @param contextualParameters the contextuals parameters
2115     * @return results
2116     */
2117    @Callable
2118    public Map<String, Object> tag(List<String> projectIds, List<String> tagNames, Map<String, Object> contextualParameters)
2119    {
2120        return tag(projectIds, tagNames, TagMode.REPLACE.toString(), contextualParameters);
2121    }
2122    
2123    /**
2124     * Tag the projects
2125     * @param projectIds the project ids
2126     * @param tagNames the tag names
2127     * @param mode the mode The mode for updating tags: 'REPLACE' to replace tags, 'INSERT' to add tags or 'REMOVE' to remove tags.
2128     * @param contextualParameters the contextual parameters
2129     * @return results
2130     */
2131    @Callable
2132    public Map<String, Object> tag(List<String> projectIds, List<String> tagNames, String mode, Map<String, Object> contextualParameters)
2133    {
2134        Map<String, Object> result = new HashMap<>();
2135        
2136        result.put("invalid-tags", new ArrayList<>());
2137        result.put("allright-projects", new ArrayList<>());
2138        
2139        for (String projectId : projectIds)
2140        {
2141            Project project = _resolver.resolveById(projectId);
2142            
2143            Map<String, Object> project2json = new HashMap<>();
2144            project2json.put("id", project.getId());
2145            project2json.put("title", project.getTitle());
2146            
2147            TagMode tagMode = TagMode.valueOf(mode);
2148            
2149            Set<String> oldTags = project.getTags();
2150            if (TagMode.REPLACE.equals(tagMode))
2151            {
2152                // First delete old tags
2153                for (String tagName : oldTags)
2154                {
2155                    project.untag(tagName);
2156                }
2157            }
2158            
2159            // Then set new tags
2160            for (String tagName : tagNames)
2161            {
2162                if (isTagValid(tagName))
2163                {
2164                    if (TagMode.REMOVE.equals(tagMode))
2165                    {
2166                        project.untag(tagName);
2167                    }
2168                    else if (TagMode.REPLACE.equals(tagMode) || !oldTags.contains(tagName))
2169                    {
2170                        project.tag(tagName);
2171                    }
2172                }
2173                else
2174                {
2175                    @SuppressWarnings("unchecked")
2176                    List<String> invalidTags = (List<String>) result.get("invalid-tags");
2177                    invalidTags.add(tagName);
2178                }
2179            }
2180            
2181            project.saveChanges();
2182            
2183            project2json.put("tags", project.getTags());
2184            @SuppressWarnings("unchecked")
2185            List<Map<String, Object>> allRightProjects = (List<Map<String, Object>>) result.get("allright-projects");
2186            allRightProjects.add(project2json);
2187            
2188            if (!oldTags.equals(project.getTags()))
2189            {
2190                // Notify observers that the project has been tagged
2191                Map<String, Object> eventParams = new HashMap<>();
2192                eventParams.put(ObservationConstants.ARGS_PROJECT, project);
2193                _observationManager.notify(new Event(ObservationConstants.EVENT_PROJECT_UPDATED, _currentUserProvider.getUser(), eventParams));
2194            }
2195        }
2196        
2197        return result;
2198    }
2199    
2200    /**
2201     * Test if a tag is valid
2202     * @param tagName The tag name
2203     * @return True if the tag is valid
2204     */
2205    public boolean isTagValid (String tagName)
2206    {
2207        Map<String, Object> params = new HashMap<>();
2208        Tag tag = _projectTagProviderEP.getTag(tagName, params);
2209        
2210        return tag != null;
2211    }
2212    
2213    /**
2214     * Get the site name holding the catalog of projects
2215     * @return the catalog's site name
2216     * @throws UnknownCatalogSiteException when the config is invalid
2217     */
2218    public String getCatalogSiteName() throws UnknownCatalogSiteException
2219    {
2220        String catalogSiteName = Config.getInstance().getValue("workspaces.catalog.site.name");
2221        if (!_siteManager.hasSite(catalogSiteName))
2222        {
2223            throw new UnknownCatalogSiteException("Unknown site '" + catalogSiteName + "'. The global Ametys configuration is invalid for the parameter 'workspaces.catalog.site.name'");
2224        }
2225        return catalogSiteName;
2226    }
2227    
2228    /**
2229     * Get the site name holding the users directory
2230     * @return the users directory's site name
2231     * @throws UnknownUserDirectorySiteException when the config is invalid
2232     */
2233    public String getUsersDirectorySiteName() throws UnknownUserDirectorySiteException
2234    {
2235        String udSiteName = Config.getInstance().getValue("workspaces.member.userdirectory.site.name");
2236        if (!_siteManager.hasSite(udSiteName))
2237        {
2238            throw new UnknownUserDirectorySiteException("Unknown site '" + udSiteName + "'. The global Ametys configuration is invalid for the parameter 'workspaces.member.userdirectory.site.name'");
2239        }
2240        return udSiteName;
2241    }
2242    
2243    @Override
2244    public boolean supports(Event event)
2245    {
2246        return event.getId().equals(ObservationConstants.EVENT_PROJECT_DELETED)
2247                || event.getId().equals(ObservationConstants.EVENT_PROJECT_UPDATED)
2248                || event.getId().equals(ObservationConstants.EVENT_PROJECT_ADDED)
2249                || event.getId().equals(org.ametys.web.ObservationConstants.EVENT_PAGE_ADDED)
2250                || event.getId().equals(org.ametys.web.ObservationConstants.EVENT_PAGE_DELETED);
2251    }
2252
2253    public int getPriority()
2254    {
2255        return 0;
2256    }
2257
2258    public void observe(Event event, Map<String, Object> transientVars) throws Exception
2259    {
2260        clearCaches();
2261    }
2262
2263    /**
2264     * Prefix project title
2265     * @param site the site
2266     * @param title the title
2267     */
2268    public void setProjectSiteTitle(Site site, String title)
2269    {
2270        I18nizableText i18nSiteTitle = new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_DEFAULT_PROJECT_WORKSPACE_TITLE", Arrays.asList(title));
2271        site.setTitle(_i18nUtils.translate(i18nSiteTitle));
2272    }
2273    
2274    private Project _computeProject(String projectName)
2275    {
2276        if (hasProject(projectName))
2277        {
2278            String uuid = _getUUIDCache().get(projectName);
2279            return _resolver.<Project>resolveById(uuid);
2280        }
2281        else
2282        {
2283            // Project may be present in cache for 'default' workspace but does not exist in current JCR workspace
2284            throw new UnknownAmetysObjectException("There is no site named '" + projectName + "'");
2285        }
2286    }
2287    
2288    /**
2289     * Check rights to create project
2290     * @param inscriptionStatus the inscription status
2291     * @param catalogPage the catalog page where projects are created
2292     */
2293    public void checkRightsForProjectCreation(InscriptionStatus inscriptionStatus, SitemapElement catalogPage)
2294    {
2295        switch (inscriptionStatus)
2296        {
2297            case PRIVATE:
2298                boolean hasRightToCreatePrivateProjetOnPages = catalogPage != null ? _rightManager.currentUserHasRight(ProjectConstants.RIGHT_PROJECT_CREATE_PRIVATE, catalogPage) == RightResult.RIGHT_ALLOW : false;
2299                if (!hasRightToCreatePrivateProjetOnPages && _rightManager.currentUserHasRight(ProjectConstants.RIGHT_PROJECT_CREATE_PRIVATE, "/${WorkspaceName}") != RightResult.RIGHT_ALLOW)
2300                {
2301                    throw new IllegalAccessError("Can't have rights to create private project");
2302                }
2303                break;
2304            case MODERATED:
2305                boolean hasRightToCreateModeratedProjetOnPages = catalogPage != null ? _rightManager.currentUserHasRight(ProjectConstants.RIGHT_PROJECT_CREATE_PUBLIC_MODERATED, catalogPage) == RightResult.RIGHT_ALLOW : false;
2306                if (!hasRightToCreateModeratedProjetOnPages && _rightManager.currentUserHasRight(ProjectConstants.RIGHT_PROJECT_CREATE_PUBLIC_MODERATED, "/${WorkspaceName}") != RightResult.RIGHT_ALLOW)
2307                {
2308                    throw new IllegalAccessError("Can't have rights to create public project with moderation");
2309                }
2310                break;
2311            case OPEN:
2312                boolean hasRightToCreateOpenProjetOnPages = catalogPage != null ? _rightManager.currentUserHasRight(ProjectConstants.RIGHT_PROJECT_CREATE_PUBLIC_OPENED, catalogPage) == RightResult.RIGHT_ALLOW : false;
2313                if (!hasRightToCreateOpenProjetOnPages && _rightManager.currentUserHasRight(ProjectConstants.RIGHT_PROJECT_CREATE_PUBLIC_OPENED, "/${WorkspaceName}") != RightResult.RIGHT_ALLOW)
2314                {
2315                    throw new IllegalAccessError("Can't have rights to create public project");
2316                }
2317                break;
2318            default:
2319                throw new IllegalArgumentException("Inscription status '" + inscriptionStatus.toString() + "' is unknown");
2320        }
2321    }
2322    
2323    /**
2324     * Check rights to edit project
2325     * @param project the project
2326     * @param inscriptionStatus the inscription status
2327     * @param catalogPage the catalog page where projects are created
2328     */
2329    public void checkRightsForProjectEdition(Project project, InscriptionStatus inscriptionStatus, SitemapElement catalogPage)
2330    {
2331        InscriptionStatus oldInscriptionStatus = project.getInscriptionStatus();
2332        if (oldInscriptionStatus != inscriptionStatus)
2333        {
2334            boolean canCreatePrivateProjet = _rightManager.currentUserHasRight(ProjectConstants.RIGHT_PROJECT_CREATE_PRIVATE, "/${WorkspaceName}") == RightResult.RIGHT_ALLOW
2335                    || (catalogPage != null ? _rightManager.currentUserHasRight(ProjectConstants.RIGHT_PROJECT_CREATE_PRIVATE, catalogPage) == RightResult.RIGHT_ALLOW : false);
2336            boolean canCreatePublicProjetWithModeration = _rightManager.currentUserHasRight(ProjectConstants.RIGHT_PROJECT_CREATE_PUBLIC_MODERATED, "/${WorkspaceName}") == RightResult.RIGHT_ALLOW
2337                    || (catalogPage != null ? _rightManager.currentUserHasRight(ProjectConstants.RIGHT_PROJECT_CREATE_PUBLIC_MODERATED, catalogPage) == RightResult.RIGHT_ALLOW : false);
2338            boolean canCreatePublicProjet = _rightManager.currentUserHasRight(ProjectConstants.RIGHT_PROJECT_CREATE_PUBLIC_OPENED, "/${WorkspaceName}") == RightResult.RIGHT_ALLOW
2339                    || (catalogPage != null ? _rightManager.currentUserHasRight(ProjectConstants.RIGHT_PROJECT_CREATE_PUBLIC_OPENED, catalogPage) == RightResult.RIGHT_ALLOW : false);
2340            
2341            switch (oldInscriptionStatus)
2342            {
2343                case PRIVATE:
2344                    if (!canCreatePrivateProjet)
2345                    {
2346                        throw new IllegalAccessError("Can't have rights to change the inscription status of private project");
2347                    }
2348                    break;
2349                case MODERATED:
2350                    if (!canCreatePublicProjetWithModeration)
2351                    {
2352                        throw new IllegalAccessError("Can't have rights to change the inscription status of public project with moderation");
2353                    }
2354                    break;
2355                case OPEN:
2356                    if (!canCreatePublicProjet)
2357                    {
2358                        throw new IllegalAccessError("Can't have rights to change the inscription status of public project");
2359                    }
2360                    break;
2361                default:
2362                    throw new IllegalArgumentException("Inscription status '" + oldInscriptionStatus.toString() + "' is unknown");
2363            }
2364            
2365            switch (inscriptionStatus)
2366            {
2367                case PRIVATE:
2368                    if (!canCreatePrivateProjet)
2369                    {
2370                        throw new IllegalAccessError("Can't have rights to change the project to private project");
2371                    }
2372                    break;
2373                case MODERATED:
2374                    if (!canCreatePublicProjetWithModeration)
2375                    {
2376                        throw new IllegalAccessError("Can't have rights to change the project to public project with moderation");
2377                    }
2378                    break;
2379                case OPEN:
2380                    if (!canCreatePublicProjet)
2381                    {
2382                        throw new IllegalAccessError("Can't have rights to change the project to public project");
2383                    }
2384                    break;
2385                default:
2386                    throw new IllegalArgumentException("Inscription status '" + inscriptionStatus.toString() + "' is unknown");
2387            }
2388        }
2389    }
2390    
2391    /**
2392     * Clear the site cache
2393     */
2394    public void clearCaches ()
2395    {
2396        _getMemorySiteAssociationCache().invalidateAll();
2397        _getMemoryProjectCache().invalidateAll();
2398        _getMemoryPageCache().invalidateAll();
2399        _getRequestProjectCache().invalidateAll();
2400        _getRequestPageCache().invalidateAll();
2401    }
2402    
2403    private Cache<String, List<Pair<String, String>>> _getMemorySiteAssociationCache()
2404    {
2405        return _cacheManager.get(MEMORY_SITEASSOCIATION_CACHE);
2406    }
2407    
2408    private Cache<String, String> _getMemoryProjectCache()
2409    {
2410        return _cacheManager.get(MEMORY_PROJECTIDBYNAMECACHE);
2411    }
2412    
2413    private Cache<ModuleCacheKey, Set<String>> _getMemoryPageCache()
2414    {
2415        return _cacheManager.get(MEMORY_PAGESBYIDCACHE);
2416    }
2417    
2418    private Cache<RequestProjectCacheKey, Project> _getRequestProjectCache()
2419    {
2420        return _cacheManager.get(REQUEST_PROJECTBYID_CACHE);
2421    }
2422    
2423    private Cache<RequestModuleCacheKey, Set<Page>> _getRequestPageCache()
2424    {
2425        return _cacheManager.get(REQUEST_PAGESBYPROJECTANDMODULE_CACHE);
2426    }
2427
2428    
2429    /**
2430     * Creates the caches
2431     */
2432    protected void _createCaches()
2433    {
2434        _cacheManager.createMemoryCache(MEMORY_SITEASSOCIATION_CACHE,
2435                new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_CACHE_PROJECT_MANAGER_LABEL"),
2436                new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_CACHE_PROJECT_MANAGER_DESCRIPTION"),
2437                true,
2438                null);
2439        _cacheManager.createMemoryCache(MEMORY_PROJECTIDBYNAMECACHE,
2440                new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_PROJECT_MANAGER_UUID_CACHE_LABEL"),
2441                new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_PROJECT_MANAGER_UUID_CACHE_DESCRIPTION"),
2442                true,
2443                null);
2444        _cacheManager.createMemoryCache(MEMORY_PAGESBYIDCACHE,
2445                new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_PROJECT_MANAGER_PAGEUUID_CACHE_LABEL"),
2446                new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_PROJECT_MANAGER_PAGEUUID_CACHE_DESCRIPTION"),
2447                true,
2448                null);
2449        _cacheManager.createRequestCache(REQUEST_PROJECTBYID_CACHE,
2450                new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_PROJECT_MANAGER_REQUEST_CACHE_LABEL"),
2451                new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_PROJECT_MANAGER_REQUEST_CACHE_DESCRIPTION"),
2452                false);
2453        _cacheManager.createRequestCache(REQUEST_PAGESBYPROJECTANDMODULE_CACHE,
2454                new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_PROJECT_MANAGER_PAGEREQUEST_CACHE_LABEL"),
2455                new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_PROJECT_MANAGER_PAGEREQUEST_CACHE_DESCRIPTION"),
2456                false);
2457    }
2458    
2459    private synchronized Map<String, String> _getUUIDCache()
2460    {
2461        if (!_getMemoryProjectCache().hasKey(__IS_CACHE_FILLED))
2462        {
2463            Session defaultSession = null;
2464            try
2465            {
2466                // Force default workspace to execute query
2467                defaultSession = _repository.login(RepositoryConstants.DEFAULT_WORKSPACE);
2468                
2469                String jcrQuery = "//element(*, ametys:project)";
2470                
2471                AmetysObjectIterable<Project> projects = _resolver.query(jcrQuery, defaultSession);
2472                
2473                for (Project project : projects)
2474                {
2475                    _getMemoryProjectCache().put(project.getName(), project.getId());
2476                }
2477                
2478                _getMemoryProjectCache().put(__IS_CACHE_FILLED, null);
2479            }
2480            catch (RepositoryException e)
2481            {
2482                throw new AmetysRepositoryException(e);
2483            }
2484            finally
2485            {
2486                if (defaultSession != null)
2487                {
2488                    defaultSession.logout();
2489                }
2490            }
2491        }
2492        
2493        Map<String, String> cacheAsMap = _getMemoryProjectCache().asMap();
2494        cacheAsMap.remove(__IS_CACHE_FILLED);
2495        return cacheAsMap;
2496    }
2497    
2498    private static final class RequestProjectCacheKey extends AbstractCacheKey
2499    {
2500        private RequestProjectCacheKey(String projectName, String workspaceName)
2501        {
2502            super(projectName, workspaceName);
2503        }
2504        
2505        static RequestProjectCacheKey of(String projectName, String workspaceName)
2506        {
2507            return new RequestProjectCacheKey(projectName, workspaceName);
2508        }
2509    }
2510    
2511    private static final class ModuleCacheKey extends AbstractCacheKey
2512    {
2513        private ModuleCacheKey(String projectName, String moduleId)
2514        {
2515            super(projectName, moduleId);
2516        }
2517        
2518        static ModuleCacheKey of(String projectName, String moduleId)
2519        {
2520            return new ModuleCacheKey(projectName, moduleId);
2521        }
2522    }
2523    
2524    private static final class RequestModuleCacheKey extends AbstractCacheKey
2525    {
2526        private RequestModuleCacheKey(String projectName, String moduleId, String workspaceName)
2527        {
2528            super(projectName, moduleId, workspaceName);
2529        }
2530        
2531        static RequestModuleCacheKey of(String projectName, String moduleId, String workspaceName)
2532        {
2533            return new RequestModuleCacheKey(projectName, moduleId, workspaceName);
2534        }
2535    }
2536    
2537    private Request _getRequest ()
2538    {
2539        try
2540        {
2541            return (Request) _context.get(ContextHelper.CONTEXT_REQUEST_OBJECT);
2542        }
2543        catch (ContextException ce)
2544        {
2545            getLogger().info("Unable to get the request", ce);
2546            return null;
2547        }
2548    }
2549
2550    /**
2551     * Retrieves all projects for client side
2552     * @return the projects
2553     */
2554    @Callable(rights = "Runtime_Rights_Admin_Access", context = "/admin")
2555    public List<Map<String, Object>> getProjectsStatisticsForClientSide()
2556    {
2557        return getProjects()
2558                .stream()
2559                .map(p -> getProjectStatistics(p))
2560                .collect(Collectors.toList());
2561    }
2562    
2563    /**
2564     * Retrieves the standard information of a project
2565     * @param project The project
2566     * @return The map of information
2567     */
2568    public Map<String, Object> getProjectStatistics(Project project)
2569    {
2570        Map<String, Object> statistics = new HashMap<>();
2571
2572        statistics.put("title", project.getTitle());
2573
2574        long totalSize = 0;
2575        for (WorkspaceModule moduleManager : _moduleManagerEP.getModules())
2576        {
2577            Map<String, Object> moduleStatistics = moduleManager.getStatistics(project);
2578            statistics.putAll(moduleStatistics);
2579            Long size = (Long) moduleStatistics.get(moduleManager.getModuleSizeKey());
2580            totalSize += (size != null && size >= 0) ? (Long) moduleStatistics.get(moduleManager.getModuleSizeKey()) : 0;
2581        }
2582
2583        statistics.put("totalSize", totalSize);
2584
2585        ZonedDateTime creationDate = project.getCreationDate();
2586        
2587        statistics.put("creationDate", creationDate);
2588        statistics.put("managers", Arrays.stream(project.getManagers())
2589                .map(u -> _userHelper.user2json(u))
2590                .collect(Collectors.toList()));
2591
2592        return statistics;
2593    }
2594
2595    /**
2596     * Retrieves all projects for client side
2597     * @return the projects
2598     */
2599    @Callable(rights = "Runtime_Rights_Admin_Access", context = "/admin")
2600    public List<Map<String, Object>> getProjectsStatisticsColumnsModel()
2601    {
2602        return getStatisticHeaders()
2603                .stream()
2604                .map(p -> p.convertToJSON())
2605                .collect(Collectors.toList());
2606    }
2607
2608    private List<StatisticColumn> getStatisticHeaders()
2609    {
2610
2611        List<StatisticColumn> flatStatisticHeaders = getFlatStatisticHeaders();
2612        List<StatisticColumn> headers = new ArrayList<>();
2613        for (StatisticColumn statisticColumn : flatStatisticHeaders)
2614        {
2615            // this column have a parent, we have to find it and attach it
2616            if (statisticColumn.getGroup() != null)
2617            {
2618                Optional<StatisticColumn> parent = flatStatisticHeaders.stream()
2619                                                    .filter(column -> column.getId().equals(statisticColumn.getGroup()))
2620                                                    .findAny();
2621                if (parent.isPresent())
2622                {
2623                    parent.get().addSubColumn(statisticColumn);
2624                }
2625            }
2626            else
2627            {
2628                headers.add(statisticColumn);
2629            }
2630        }
2631        
2632        return headers;
2633    }
2634    
2635    private List<StatisticColumn> getFlatStatisticHeaders()
2636    {
2637        List<StatisticColumn> headers = new ArrayList<>();
2638        headers.add(new StatisticColumn("title", new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_PROJECT_STATISTICS_TOOL_COLUMN_TITLE"))
2639                .withType(StatisticsColumnType.STRING)
2640                .withWidth(200)
2641                .withRenderer("Ametys.plugins.workspaces.project.tool.ProjectsGridHelper.renderTitle"));
2642        headers.add(new StatisticColumn("creationDate", new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_PROJECT_STATISTICS_TOOL_COLUMN_CREATION"))
2643                .withType(StatisticsColumnType.DATE)
2644                .withWidth(150));
2645        headers.add(new StatisticColumn("managers", new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_PROJECT_STATISTICS_TOOL_COLUMN_MANAGERS"))
2646                .withRenderer("Ametys.grid.GridColumnHelper.renderUser")
2647                .withFilter(false));
2648        for (WorkspaceModule moduleManager : _moduleManagerEP.getModules())
2649        {
2650            headers.addAll(moduleManager.getStatisticModel());
2651        }
2652        
2653        StatisticColumn elements = new StatisticColumn(WorkspaceModule.GROUP_HEADER_ELEMENTS_ID, new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_PROJECT_STATISTICS_TOOL_COLUMN_ELEMENTS"))
2654                .withFilter(false);
2655        headers.add(elements);
2656
2657        StatisticColumn activatedModules = new StatisticColumn(WorkspaceModule.GROUP_HEADER_ACTIVATED_ID, new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_PROJECT_STATISTICS_TOOL_COLUMN_ACTIVE_MODULES"))
2658                .isHidden(true)
2659                .withFilter(false);
2660        headers.add(activatedModules);
2661        
2662        StatisticColumn lastActivity = new StatisticColumn(WorkspaceModule.GROUP_HEADER_LAST_ACTIVITY_ID, new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_PROJECT_STATISTICS_TOOL_COLUMN_LAST_ACTIVITY")).isHidden(true);
2663        headers.add(lastActivity);
2664
2665        StatisticColumn modulesSize = new StatisticColumn(WorkspaceModule.GROUP_HEADER_SIZE_ID, new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_PROJECT_STATISTICS_TOOL_COLUMN_MODULES_SIZE"))
2666                .withFilter(false);
2667        modulesSize.addSubColumn(new StatisticColumn("totalSize", new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_PROJECT_STATISTICS_TOOL_COLUMN_MODULES_SIZE_TOTAL"))
2668                .withRenderer("Ametys.plugins.workspaces.project.tool.ProjectsGridHelper.renderSize")
2669                .withType(StatisticsColumnType.LONG));
2670        
2671        headers.add(modulesSize);
2672        
2673        return headers;
2674    }
2675    
2676    /**
2677     * Check if the user is in one of the populations of project
2678     * @param project the project
2679     * @param user the user
2680     * @return true if the user is in one of the populations of project
2681     */
2682    public boolean isUserInProjectPopulations(Project project, UserIdentity user)
2683    {
2684        Site site = project.getSite();
2685
2686        if (site == null)
2687        {
2688            throw new IllegalArgumentException("Cannot determine if user " + UserIdentity.userIdentityToString(user) + " can connect to the project " + project.getName() + " since the project has no associated site to determine the populations");
2689        }
2690        String siteName = site.getName();
2691
2692        Set<String> populations = _populationContextHelper.getUserPopulationsOnContext("/sites/" + siteName, false);
2693        Set<String> frontPopulations = _populationContextHelper.getUserPopulationsOnContext("/sites-fo/" + siteName, false);
2694        
2695        return populations.contains(user.getPopulationId()) || frontPopulations.contains(user.getPopulationId());
2696    }
2697    
2698    /**
2699     * Thrown to indicate that the catalog site is unknown
2700     */
2701    public class UnknownCatalogSiteException extends IllegalArgumentException
2702    {
2703        /**
2704         * Construct a {@code UnknownCatalogSiteException} with the specified message
2705         * @param message the message
2706         */
2707        public UnknownCatalogSiteException(String message)
2708        {
2709            super(message);
2710        }
2711    }
2712    
2713    /**
2714     * Thrown to indicate that the user directory site is unknown
2715     */
2716    public class UnknownUserDirectorySiteException extends IllegalArgumentException
2717    {
2718        /**
2719         * Construct a {@code UnknownUserDirectorySiteException} with the specified message
2720         * @param message the message
2721         */
2722        public UnknownUserDirectorySiteException(String message)
2723        {
2724            super(message);
2725        }
2726    }
2727}