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