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