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