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