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.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.runtime.config.Config;
113import org.ametys.runtime.i18n.I18nizableText;
114import org.ametys.runtime.plugin.component.PluginAware;
115import org.ametys.web.repository.page.ModifiablePage;
116import org.ametys.web.repository.page.Page;
117import org.ametys.web.repository.page.PageQueryHelper;
118import org.ametys.web.repository.page.PagesContainer;
119import org.ametys.web.repository.site.Site;
120import org.ametys.web.repository.site.SiteDAO;
121import org.ametys.web.repository.site.SiteManager;
122import org.ametys.web.repository.sitemap.Sitemap;
123import org.ametys.web.site.SiteConfigurationManager;
124
125import com.google.common.collect.Iterables;
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 = Iterables.getFirst(project.getSites(), (Site) null);
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.getSites().iterator().next();
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", Iterables.getFirst(getProjectUrls(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        Collection<Site> sites = project.getSites();
757        boolean valid = sites.size() > 0;
758        if (valid)
759        {
760            Iterator<Site> siteIterator = sites.iterator();
761            while (valid && siteIterator.hasNext())
762            {
763                Site site = siteIterator.next();
764                valid = _siteConfigurationManager.isSiteConfigurationValid(site);
765            }
766        }
767        
768        Set<String> categories = project.getCategories();
769        info.put("categories", categories.stream()
770                .map(c -> _categoryProviderEP.getTag(c, new HashMap<>()))
771                .filter(Objects::nonNull)
772                .map(t -> _tag2json(t))
773                .collect(Collectors.toList()));
774        
775        Set<String> tags = project.getTags();
776        info.put("tags", tags.stream()
777                .map(c -> _projectTagProviderEP.getTag(c, new HashMap<>()))
778                .filter(Objects::nonNull)
779                .map(t -> _tag2json(t))
780                .collect(Collectors.toList()));
781        
782        info.put("valid", valid);
783        
784        UserIdentity[] managers = project.getManagers();
785        info.put("managers", Arrays.stream(managers)
786                .map(u -> _userHelper.user2json(u))
787                .collect(Collectors.toList()));
788        
789        // sites is a list of map entry with id ,name, title and url property
790        // { id: site id, name: site name, title: site title, url: site url }
791        info.put("sites", sites.stream().map(site ->
792        {
793            Map<String, String> siteProps = new HashMap<>();
794            siteProps.put("id", site.getId());
795            siteProps.put("name", site.getName());
796            siteProps.put("title", site.getTitle());
797            siteProps.put("url", site.getUrl());
798            return siteProps;
799        }).collect(Collectors.toList()));
800
801        return info;
802    }
803    
804    private Map<String, Object> _tag2json(Tag tag)
805    {
806        Map<String, Object> json = new HashMap<>();
807        json.put("id", tag.getId());
808        json.put("name", tag.getName());
809        json.put("title", tag.getTitle());
810        return json;
811    }
812    
813    /**
814     * Get the availables project URLs.
815     * @param project The project
816     * @return The availables project URLs, can be empty.
817     */
818    public Set<String> getProjectUrls(Project project)
819    {
820        return _getProjectNonEmptyElements(project, Site::getUrl);
821    }
822    
823    /**
824     * Get the availables project names.
825     * @param project The project
826     * @return The availables project names, can be empty.
827     */
828    public Set<String> getProjectNames(Project project)
829    {
830        return _getProjectNonEmptyElements(project, Site::getName);
831    }
832    
833    private Set<String> _getProjectNonEmptyElements(Project project, Function<? super Site, ? extends String> function)
834    {
835        return project.getSites()                       // Get the sites of the project
836                      .stream()                         // Build it as a stream
837                      .map(function)                    // Get the element of each site
838                      .filter(StringUtils::isNotEmpty)  // Filter empty strings
839                      .collect(Collectors.toSet());     // Get only the first value
840    }
841    
842    /**
843     * Create a project
844     * @param name The project name
845     * @param title The project title
846     * @param description The project description
847     * @param emailList Project mailing list
848     * @param inscriptionStatus The inscription status of the project
849     * @param defaultProfile The default profile for new members
850     * @return A map containing the id of the new project or an error key.
851     */
852    @Callable
853    public Map<String, Object> createProject(String name, String title, String description, String emailList, String inscriptionStatus, String defaultProfile)
854    {
855        checkRightsForProjectCreation(InscriptionStatus.valueOf(inscriptionStatus.toUpperCase()));
856
857        Map<String, Object> result = new HashMap<>();
858        List<String> errors = new ArrayList<>();
859        
860        Map<String, Object> additionalValues = new HashMap<>();
861        additionalValues.put("description", description);
862        additionalValues.put("emailList", emailList);
863        additionalValues.put("inscriptionStatus", inscriptionStatus);
864        additionalValues.put("defaultProfile", defaultProfile);
865        
866        Project project = createProject(name, title, additionalValues, null, errors);
867        
868        if (CollectionUtils.isEmpty(errors))
869        {
870            result.put("id", project.getId());
871        }
872        else
873        {
874            result.put("error", errors.get(0));
875        }
876        
877        return result;
878    }
879
880    /**
881     * Create a project
882     * @param name The project name
883     * @param title The project title
884     * @param additionalValues A list of optional additional values. Accepted values are : description, mailingList, inscriptionStatus, defaultProfile, tags, categoryTags, keywords and language
885     * @param modulesIds The list of modules to activate. Can be null to activate all modules
886     * @param errors A list that will be populated with the encountered errors. If null, errors will not be tracked.
887     * @return The id of the new project
888     */
889    public Project createProject(String name, String title, Map<String, Object> additionalValues, Set<String> modulesIds, List<String> errors)
890    {
891        if (StringUtils.isEmpty(title))
892        {
893            throw new IllegalArgumentException(String.format("Cannot create project. Title is mandatory"));
894        }
895        
896        ModifiableTraversableAmetysObject projectsRoot = getProjectsRoot();
897        
898        // Project name should be unique
899        if (hasProject(name))
900        {
901            if (getLogger().isWarnEnabled())
902            {
903                getLogger().warn(String.format("A project with the name '%s' already exists", name));
904            }
905            
906            if (errors != null)
907            {
908                errors.add("project-exists");
909            }
910            
911            return null;
912        }
913        
914        Project project = projectsRoot.createChild(name, Project.NODE_TYPE);
915        project.setTitle(title);
916        String description = (String) additionalValues.getOrDefault("description", null);
917        if (StringUtils.isNotEmpty(description))
918        {
919            project.setDescription(description);
920        }
921        String mailingList = (String) additionalValues.getOrDefault("emailList", null);
922        if (StringUtils.isNotEmpty(mailingList))
923        {
924            project.setMailingList(mailingList);
925        }
926        
927        String inscriptionStatus = (String) additionalValues.getOrDefault("inscriptionStatus", null);
928        if (StringUtils.isNotEmpty(inscriptionStatus))
929        {
930            project.setInscriptionStatus(inscriptionStatus);
931        }
932        
933        String defaultProfile = (String) additionalValues.getOrDefault("defaultProfile", null);
934        if (StringUtils.isNotEmpty(defaultProfile))
935        {
936            project.setDefaultProfile(defaultProfile);
937        }
938        
939        @SuppressWarnings("unchecked")
940        List<String> tags = (List<String>) additionalValues.getOrDefault("tags", null);
941        if (tags != null)
942        {
943            project.setTags(tags);
944        }
945        @SuppressWarnings("unchecked")
946        List<String> categoryTags = (List<String>) additionalValues.getOrDefault("categoryTags", null);
947        if (categoryTags != null)
948        {
949            project.setCategoryTags(categoryTags);
950        }
951        
952        @SuppressWarnings("unchecked")
953        List<String> keywords = (List<String>) additionalValues.getOrDefault("keywords", null);
954        if (keywords != null)
955        {
956            project.setKeywords(keywords.toArray(new String[keywords.size()]));
957        }
958
959        project.setCreationDate(ZonedDateTime.now());
960        
961        // Create the project workspace = a site + a set of pages
962        _createProjectWorkspace(project, errors);
963        
964        activateModules(project, modulesIds, additionalValues);
965        
966        if (CollectionUtils.isEmpty(errors))
967        {
968            project.saveChanges();
969         
970            // Notify observers
971            Map<String, Object> eventParams = new HashMap<>();
972            eventParams.put(ObservationConstants.ARGS_PROJECT, project);
973            _observationManager.notify(new Event(ObservationConstants.EVENT_PROJECT_ADDED, _currentUserProvider.getUser(), eventParams));
974            
975        }
976        else
977        {
978            deleteProject(project);
979        }
980        
981        clearCaches();
982        
983        return project;
984    }
985    
986    /**
987     * Edit a project
988     * @param id The project identifier
989     * @param title The title to set
990     * @param description The description to set
991     * @param mailingList Project mailing list
992     * @param inscriptionStatus The inscription status of the project
993     * @param defaultProfile The default profile for new members
994     */
995    @Callable(right = ProjectConstants.RIGHT_PROJECT_EDIT, context = "/admin")
996    public void editProject(String id, String title, String description, String mailingList, String inscriptionStatus, String defaultProfile)
997    {
998        Project project = _resolver.resolveById(id);
999        editProject(project, title, description, mailingList, inscriptionStatus, defaultProfile);
1000    }
1001    
1002    /**
1003     * Edit a project
1004     * @param project The project
1005     * @param title The title to set
1006     * @param description The description to set
1007     * @param mailingList Project mailing list
1008     * @param inscriptionStatus The inscription status of the project
1009     * @param defaultProfile The default profile for new members
1010     */
1011    public void editProject(Project project, String title, String description, String mailingList, String inscriptionStatus, String defaultProfile)
1012    {
1013        checkRightsForProjectEdition(project, InscriptionStatus.valueOf(inscriptionStatus.toUpperCase()));
1014        
1015        project.setTitle(title);
1016        
1017        if (StringUtils.isNotEmpty(description))
1018        {
1019            project.setDescription(description);
1020        }
1021        else
1022        {
1023            project.removeDescription();
1024        }
1025        
1026        if (StringUtils.isNotEmpty(mailingList))
1027        {
1028            project.setMailingList(mailingList);
1029        }
1030        else
1031        {
1032            project.removeMailingList();
1033        }
1034
1035        project.setInscriptionStatus(inscriptionStatus);
1036        project.setDefaultProfile(defaultProfile);
1037        
1038        project.saveChanges();
1039        
1040        // Notify observers
1041        Map<String, Object> eventParams = new HashMap<>();
1042        eventParams.put(ObservationConstants.ARGS_PROJECT, project);
1043        _observationManager.notify(new Event(ObservationConstants.EVENT_PROJECT_UPDATED, _currentUserProvider.getUser(), eventParams));
1044    }
1045    
1046    /**
1047     * Delete a list of project.
1048     * @param ids The ids of projects to delete
1049     * @return The ids of the deleted projects, unknowns projects and the deleted sites
1050     */
1051    @Callable(right = ProjectConstants.RIGHT_PROJECT_DELETE, context = "/admin")
1052    public Map<String, Object> deleteProjectsByIds(List<String> ids)
1053    {
1054        Map<String, Object> result = new HashMap<>();
1055        List<Map<String, Object>> deleted = new ArrayList<>();
1056        List<String> unknowns = new ArrayList<>();
1057        
1058        for (String id : ids)
1059        {
1060            try
1061            {
1062                Project project = _resolver.resolveById(id);
1063                
1064                Map<String, Object> projectInfo = new HashMap<>();
1065                projectInfo.put("id", id);
1066                projectInfo.put("title", project.getTitle());
1067                projectInfo.put("sites", deleteProject(project));
1068                
1069                deleted.add(projectInfo);
1070            }
1071            catch (UnknownAmetysObjectException e)
1072            {
1073                getLogger().warn(String.format("Unable to delete the definition of id '%s', because it does not exist.", id), e);
1074                unknowns.add(id);
1075            }
1076        }
1077        
1078        result.put("deleted", deleted);
1079        result.put("unknowns", unknowns);
1080        
1081        return result;
1082    }
1083    
1084    /**
1085     * Delete a project.
1086     * @param projects The list of projects to delete
1087     * @return list of deleted sites (each list entry contains a data map with
1088     *         the id and the name of the delete site).
1089     */
1090    public List<Map<String, String>> deleteProject(List<Project> projects)
1091    {
1092        List<Map<String, String>> deletedSitesInfo = new ArrayList<>();
1093        
1094        for (Project project : projects)
1095        {
1096            deletedSitesInfo.addAll(deleteProject(project));
1097        }
1098        
1099        return deletedSitesInfo;
1100    }
1101    
1102    /**
1103     * Delete a project and its sites
1104     * @param project The project to delete
1105     * @return list of deleted sites (each list entry contains a data map with
1106     *         the id and the name of the delete site).
1107     */
1108    public List<Map<String, String>> deleteProject(Project project)
1109    {
1110        ModifiableAmetysObject parent = project.getParent();
1111        
1112        Collection<Site> sites = project.getSites();
1113        
1114        // list of map entry with id, name and title property
1115        // { id: site id, name: site name }
1116        List<Map<String, String>> deletedSitesInfo = new ArrayList<>();
1117        
1118        sites.forEach(site -> 
1119        {
1120            try
1121            {
1122                Map<String, String> siteProps = new HashMap<>();
1123                siteProps.put("id", site.getId());
1124                siteProps.put("name", site.getName());
1125
1126                _siteDao.deleteSite(site.getId());
1127                deletedSitesInfo.add(siteProps);
1128            }
1129            catch (RepositoryException e)
1130            {
1131                String errorMsg = String.format("Error while trying to delete the site '%s' for the project '%s'.", site.getName(), project.getName());
1132                getLogger().error(errorMsg, e);
1133            }
1134        });
1135        
1136        String projectId = project.getId();
1137        Collection<ProjectMember> projectMembers = _projectMembers.getProjectMembers(project, true);
1138        project.remove();
1139        parent.saveChanges();
1140        
1141        // Notify observers
1142        Map<String, Object> eventParams = new HashMap<>();
1143        eventParams.put(ObservationConstants.ARGS_PROJECT_ID, projectId);
1144        eventParams.put(ObservationConstants.ARGS_PROJECT_NAME, project.getName());
1145        eventParams.put(ObservationConstants.ARGS_PROJECT_MEMBERS, projectMembers);
1146        _observationManager.notify(new Event(ObservationConstants.EVENT_PROJECT_DELETED, _currentUserProvider.getUser(), eventParams));
1147        
1148        clearCaches();
1149        
1150        return deletedSitesInfo;
1151    }
1152    
1153    /**
1154     * Utility method to get or create an ametys object
1155     * @param <A> A sub class of AmetysObject
1156     * @param parent The parent object
1157     * @param name The ametys object name
1158     * @param type The ametys object type
1159     * @return ametys object
1160     * @throws AmetysRepositoryException if an repository error occurs
1161     */
1162    private <A extends AmetysObject> A _getOrCreateObject(ModifiableTraversableAmetysObject parent, String name, String type) throws AmetysRepositoryException
1163    {
1164        A object;
1165        
1166        if (parent.hasChild(name))
1167        {
1168            object = parent.getChild(name);
1169        }
1170        else
1171        {
1172            object = parent.createChild(name, type);
1173            parent.saveChanges();
1174        }
1175        
1176        return object;
1177    }
1178    
1179    /**
1180     * Get the project of an ametys object inside a project.
1181     * It can be an explorer node, or any type of resource in a module.
1182     * @param id The identifier of the ametys object
1183     * @return the project or null if not found
1184     */
1185    public Project getParentProject(String id)
1186    {
1187        return getParentProject(_resolver.<AmetysObject>resolveById(id));
1188    }
1189    
1190    /**
1191     * Get the project of an ametys object inside a project.
1192     * It can be an explorer node, or any type of resource in a module.
1193     * @param object The ametys object
1194     * @return the project or null if not found
1195     */
1196    public Project getParentProject(AmetysObject object)
1197    {
1198        AmetysObject ametysObject = object;
1199        // Go back to the local explorer root.
1200        do
1201        {
1202            ametysObject = ametysObject.getParent();
1203        }
1204        while (ametysObject instanceof ExplorerNode);
1205        
1206        if (!(ametysObject instanceof Project))
1207        {
1208            getLogger().warn(String.format("No project found for ametys object with id '%s'", ametysObject.getId()));
1209            return null;
1210        }
1211        
1212        return (Project) ametysObject; 
1213    }
1214    
1215    /**
1216     * Get the list of project names for a given site
1217     * @param siteName The site name
1218     * @return the list of project names
1219     */
1220    @Callable(right = "Runtime_Rights_Admin_Access", context = "/admin")
1221    public List<String> getProjectsForSite(String siteName)
1222    {
1223        Cache<String, List<Pair<String, String>>> cache = _getMemorySiteAssociationCache();
1224        if (cache.hasKey(siteName))
1225        {
1226            return cache.get(siteName).stream()
1227                    .map(p -> p.getRight())
1228                    .collect(Collectors.toList());
1229        }
1230        else
1231        {
1232            List<String> projectNames = new ArrayList<>();
1233            
1234            if (_siteManager.hasSite(siteName))
1235            {
1236                Site site = _siteManager.getSite(siteName);
1237                getProjectsForSite(site)
1238                    .stream()
1239                    .map(Project::getName)
1240                    .forEach(projectNames::add);
1241            }
1242            
1243            return projectNames;
1244        }
1245    }
1246    
1247    /**
1248     * Get the list of project for a given site
1249     * @param site The site
1250     * @return the list of project
1251     */
1252    public List<Project> getProjectsForSite(Site site)
1253    {
1254        Cache<String, List<Pair<String, String>>> cache = _getMemorySiteAssociationCache();
1255        if (cache.hasKey(site.getName()))
1256        {
1257            cache.get(site.getName()).stream()
1258                 .map(p -> _resolver.resolveById(p.getLeft()))
1259                 .collect(Collectors.toList());
1260        }
1261        
1262        try
1263        {
1264            // Stream over the weak reference properties pointing to this
1265            // node to find referencing projects 
1266            Iterator<Property> propertyIterator = site.getNode().getWeakReferences();
1267            Iterable<Property> propertyIterable = () -> propertyIterator;
1268            
1269            List<Project> projects = StreamSupport.stream(propertyIterable.spliterator(), false)
1270                    .map(p -> 
1271                    {
1272                        try
1273                        {
1274                            // Parent should be a composite with name "ametys:sites"
1275                            Node parent = p.getParent();
1276                            if (NodeTypeHelper.isNodeType(parent, "ametys:compositeMetadata") && (RepositoryConstants.NAMESPACE_PREFIX + ":" + Project.DATA_SITES).equals(parent.getName()))
1277                            {
1278                                // Parent should be the project
1279                                parent = parent.getParent();
1280                                if (NodeTypeHelper.isNodeType(parent, "ametys:project"))
1281                                {
1282                                    Project project = _resolver.resolve(parent, false);
1283                                    return project;
1284                                }
1285                            }
1286                        }
1287                        catch (Exception e)
1288                        {
1289                            if (getLogger().isWarnEnabled())
1290                            {
1291                                // this weak reference is not from a project
1292                                String propertyPath = null;
1293                                try
1294                                {
1295                                    propertyPath = p.getPath();
1296                                }
1297                                catch (Exception e2)
1298                                {
1299                                    // ignore
1300                                }
1301                                
1302                                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);
1303                                getLogger().warn(warnMsg);
1304                            }
1305                        }
1306                        
1307                        return null;
1308                    })
1309                    .filter(Objects::nonNull)
1310                    .collect(Collectors.toList());
1311            
1312            List<Pair<String, String>> projectsPairs = projects.stream().map(p -> Pair.of(p.getId(), p.getName())).collect(Collectors.toList());
1313            cache.put(site.getName(), projectsPairs);
1314            return projects;
1315        }
1316        catch (RepositoryException e)
1317        {
1318            getLogger().error(String.format("Unable to find projects for site '%s'", site.getName()), e);
1319        }
1320        
1321        return new ArrayList<>();
1322    }
1323    
1324    /**
1325     * Create the project workspace for a given project.
1326     * @param project The project for which the workspace must be created
1327     * @param errors A list of possible errors to populate. Can be null if the caller is not interested in error tracking.
1328     * @return The site created for this workspace
1329     */
1330    protected Site _createProjectWorkspace(Project project, List<String> errors)
1331    {
1332        String initialSiteName = project.getName();
1333        Site site = null;
1334        
1335        Site catalogSite = _siteManager.getSite(getCatalogSiteName());
1336        String rootId = catalogSite != null ? catalogSite.getId() : null;
1337        
1338        Map<String, Object> result = _siteDao.createSite(rootId, initialSiteName, ProjectWorkspaceSiteType.TYPE_ID, true);
1339        
1340        String siteId = (String) result.get("id");
1341        String siteName = (String) result.get("name");
1342        if (StringUtils.isNotEmpty(siteId)) 
1343        {
1344            // Creation success
1345            site = _siteManager.getSite(siteName);
1346            
1347            setProjectSiteTitle(site, project.getTitle());
1348            
1349            // Add site to project
1350            project.setSites(Arrays.asList(site.getName()));
1351            
1352            site.saveChanges();
1353        }
1354        
1355        return site;
1356    }
1357    
1358    /**
1359     * Get the project's tags
1360     * @return The project's tags 
1361     */
1362    @Callable
1363    public List<String> getTags()
1364    {
1365        AmetysObject projectsRootNode = getProjectsRoot();
1366        if (projectsRootNode instanceof JCRAmetysObject)
1367        {
1368            Node node = ((JCRAmetysObject) projectsRootNode).getNode();
1369            
1370            try
1371            {
1372                return Arrays.stream(node.getProperty(__PROJECTS_TAGS_PROPERTY).getValues())
1373                    .map(LambdaUtils.wrap(Value::getString))
1374                    .collect(Collectors.toList());
1375            }
1376            catch (PathNotFoundException e)
1377            {
1378                // property is not set, empty list will be returned.
1379            }
1380            catch (RepositoryException e)
1381            {
1382                throw new AmetysRepositoryException(e);
1383            }
1384        }
1385        
1386        return new ArrayList<>();
1387    }
1388    
1389    /**
1390     * Set the tags
1391     * @param tags The tags to set
1392     */
1393    @Callable
1394    public synchronized void setTags(List<String> tags)
1395    {
1396        AmetysObject projectsRootNode = getProjectsRoot();
1397        if (projectsRootNode instanceof JCRAmetysObject)
1398        {
1399            JCRAmetysObject jcrProjectsRootNode = (JCRAmetysObject) projectsRootNode;
1400            
1401            if (CollectionUtils.isNotEmpty(tags))
1402            {
1403                String[] tagsArray = tags.stream()
1404                        .map(String::trim)
1405                        .map(String::toLowerCase)
1406                        .filter(StringUtils::isNotEmpty)
1407                        .distinct()
1408                        .toArray(String[]::new);
1409                
1410                try
1411                {
1412                    jcrProjectsRootNode.getNode().setProperty(__PROJECTS_TAGS_PROPERTY, tagsArray);
1413                    jcrProjectsRootNode.saveChanges();
1414                }
1415                catch (RepositoryException e)
1416                {
1417                    throw new AmetysRepositoryException(e);
1418                }
1419            }
1420            else
1421            {
1422                Node node = jcrProjectsRootNode.getNode();
1423                try
1424                {
1425                    if (node.hasProperty(__PROJECTS_TAGS_PROPERTY))
1426                    {
1427                        node.getProperty(__PROJECTS_TAGS_PROPERTY).remove();
1428                        jcrProjectsRootNode.saveChanges();
1429                    }
1430                }
1431                catch (RepositoryException e)
1432                {
1433                    throw new AmetysRepositoryException(e);
1434                }
1435            }
1436        }
1437    }
1438    
1439    /**
1440     * Add project's tags
1441     * @param newTags The new tags to add
1442     */
1443    @Callable
1444    public synchronized void addTags(Collection<String> newTags)
1445    {
1446        if (CollectionUtils.isNotEmpty(newTags))
1447        {
1448            AmetysObject projectsRootNode = getProjectsRoot();
1449            if (projectsRootNode instanceof JCRAmetysObject)
1450            {
1451                // Concat existing tags with new lowercased tags
1452                String[] tags = Stream.concat(getTags().stream(), newTags.stream().map(String::trim).map(String::toLowerCase).filter(StringUtils::isNotEmpty))
1453                        .distinct()
1454                        .toArray(String[]::new);
1455                
1456                try
1457                {
1458                    ((JCRAmetysObject) projectsRootNode).getNode().setProperty(__PROJECTS_TAGS_PROPERTY, tags);
1459                }
1460                catch (RepositoryException e)
1461                {
1462                    throw new AmetysRepositoryException(e);
1463                }
1464            }
1465        }
1466    }
1467    
1468    /**
1469     * Get the project's places
1470     * @return The project's places 
1471     */
1472    @Callable
1473    public List<String> getPlaces()
1474    {
1475        AmetysObject projectsRootNode = getProjectsRoot();
1476        if (projectsRootNode instanceof JCRAmetysObject)
1477        {
1478            Node node = ((JCRAmetysObject) projectsRootNode).getNode();
1479            
1480            try
1481            {
1482                return Arrays.stream(node.getProperty(__PROJECTS_PLACES_PROPERTY).getValues())
1483                    .map(LambdaUtils.wrap(Value::getString))
1484                    .collect(Collectors.toList());
1485            }
1486            catch (PathNotFoundException e)
1487            {
1488                // property is not set, empty list will be returned.
1489            }
1490            catch (RepositoryException e)
1491            {
1492                throw new AmetysRepositoryException(e);
1493            }
1494        }
1495        
1496        return new ArrayList<>();
1497    }
1498    
1499    /**
1500     * Add project's places
1501     * @param newPlaces The new places to add
1502     */
1503    public synchronized void addPlaces(Collection<String> newPlaces)
1504    {
1505        if (CollectionUtils.isNotEmpty(newPlaces))
1506        {
1507            AmetysObject projectsRootNode = getProjectsRoot();
1508            if (projectsRootNode instanceof JCRAmetysObject)
1509            {
1510                Set<String> lowercasedPlaces = new HashSet<>();
1511                
1512                // Concat existing places with new places
1513                String[] places = Stream.concat(getPlaces().stream(), newPlaces.stream().map(String::trim).filter(StringUtils::isNotEmpty))
1514                        // duplicates are filtered out
1515                        .filter(p -> lowercasedPlaces.add(p.toLowerCase()))
1516                        .toArray(String[]::new);
1517                
1518                try
1519                {
1520                    ((JCRAmetysObject) projectsRootNode).getNode().setProperty(__PROJECTS_PLACES_PROPERTY, places);
1521                }
1522                catch (RepositoryException e)
1523                {
1524                    throw new AmetysRepositoryException(e);
1525                }
1526            }
1527        }
1528    }
1529    
1530    /**
1531     * Set the places
1532     * @param places The places to set
1533     */
1534    @Callable
1535    public synchronized void setPlaces(List<String> places)
1536    {
1537        AmetysObject projectsRootNode = getProjectsRoot();
1538        if (projectsRootNode instanceof JCRAmetysObject)
1539        {
1540            JCRAmetysObject jcrProjectsRootNode = (JCRAmetysObject) projectsRootNode;
1541            
1542            if (CollectionUtils.isNotEmpty(places))
1543            {
1544                Set<String> lowercasedPlaces = new HashSet<>();
1545                
1546                String[] placesArray = places.stream()
1547                        .map(String::trim)
1548                        .filter(StringUtils::isNotEmpty)
1549                        // duplicates are filtered out
1550                        .filter(p -> lowercasedPlaces.add(p.toLowerCase()))
1551                        .toArray(String[]::new);
1552                
1553                try
1554                {
1555                    jcrProjectsRootNode.getNode().setProperty(__PROJECTS_PLACES_PROPERTY, placesArray);
1556                    jcrProjectsRootNode.saveChanges();
1557                }
1558                catch (RepositoryException e)
1559                {
1560                    throw new AmetysRepositoryException(e);
1561                }
1562            }
1563            else
1564            {
1565                Node node = jcrProjectsRootNode.getNode();
1566                try
1567                {
1568                    if (node.hasProperty(__PROJECTS_PLACES_PROPERTY))
1569                    {
1570                        node.getProperty(__PROJECTS_PLACES_PROPERTY).remove();
1571                        jcrProjectsRootNode.saveChanges();
1572                    }
1573                }
1574                catch (RepositoryException e)
1575                {
1576                    throw new AmetysRepositoryException(e);
1577                }
1578            }
1579        }
1580    }
1581    
1582    /**
1583     * Get the list of activated modules for a project
1584     * @param project The project
1585     * @return The list of activated modules
1586     */
1587    public List<WorkspaceModule> getModules(Project project)
1588    {
1589        return _moduleManagerEP.getModules().stream()
1590                .filter(module -> isModuleActivated(project, module.getId()))
1591                .collect(Collectors.toList());
1592    }
1593    
1594    /**
1595     * Retrieves the page of the module for all available languages
1596     * @param project The project
1597     * @param moduleId The project module id
1598     * @return the page or null if not found
1599     */
1600    public Set<Page> getModulePages(Project project, String moduleId)
1601    {
1602        if (_moduleManagerEP.hasExtension(moduleId))
1603        {
1604            WorkspaceModule module = _moduleManagerEP.getExtension(moduleId);
1605            return getModulePages(project, module);
1606        }
1607        return null;
1608    }
1609    
1610    /**
1611     * Return the possible module roots associated to a page
1612     * @param page The given page
1613     * @return A non null set of the data of the linked modules
1614     */
1615    public Set<ModifiableResourceCollection> pageToModuleRoot(Page page)
1616    {
1617        Set<ModifiableResourceCollection> data = new LinkedHashSet<>();
1618        
1619        Page rootPage = page;
1620        PagesContainer parent = page.getParent();
1621        while (!(parent instanceof Sitemap) && !page.hasValue(__PAGE_MODULES_VALUE))
1622        {
1623            rootPage = (Page) parent;
1624            parent = parent.getParent();
1625        }
1626        
1627        String[] modulesRootsIds = rootPage.getValue(__PAGE_MODULES_VALUE, new String[0]);
1628        if (modulesRootsIds.length > 0)
1629        {
1630            for (String moduleRootId : modulesRootsIds)
1631            {
1632                try
1633                {
1634                    ModifiableResourceCollection moduleRoot = _resolver.resolveById(moduleRootId);
1635                    data.add(moduleRoot);
1636                }
1637                catch (UnknownAmetysObjectException e)
1638                {
1639                    // Ignore obsolete data
1640                }
1641            }
1642        }
1643        
1644        return data;
1645    }
1646    
1647    /**
1648     * Mark the given page as this module page. The modified page will not be saved.
1649     * @param page The page to change
1650     * @param moduleRoot The workspace module that use this page
1651     */
1652    public void tagProjectPage(ModifiablePage page, ModifiableResourceCollection moduleRoot)
1653    {
1654        String[] currentModules = page.getValue(__PAGE_MODULES_VALUE, new String[0]);
1655        
1656        Set<String> modules = new LinkedHashSet<>(Arrays.asList(currentModules));
1657        modules.add(moduleRoot.getId());
1658        
1659        String[] newModules = new String[modules.size()];
1660        modules.toArray(newModules);
1661        
1662        page.setValue(__PAGE_MODULES_VALUE, newModules);
1663    }
1664
1665    /**
1666     * Remove the mark on the given page of this module. The modified page will not be saved.
1667     * @param page The page to change
1668     * @param moduleRoot The workspace module that use this page
1669     */
1670    public void untagProjectPage(ModifiablePage page, ModifiableResourceCollection moduleRoot)
1671    {
1672        if (moduleRoot != null)
1673        {
1674            String[] currentModules = page.getValue(__PAGE_MODULES_VALUE, new String[0]);
1675            
1676            Set<String> modules = new LinkedHashSet<>(Arrays.asList(currentModules));
1677            modules.remove(moduleRoot.getId());
1678            
1679            String[] newModules = new String[modules.size()];
1680            modules.toArray(newModules);
1681            
1682            page.setValue(__PAGE_MODULES_VALUE, newModules);
1683        }
1684    }
1685
1686    /**
1687     * Get a page in the site of a given project with a specific tag
1688     * @param project The project
1689     * @param workspaceModule the module
1690     * @return The module's pages
1691     */
1692    public Set<Page> getModulePages(Project project, WorkspaceModule workspaceModule)
1693    {
1694        Request request = _getRequest();
1695        if (request == null)
1696        {
1697            // There is no request to store cache
1698            return _computePages(project, workspaceModule);
1699        }
1700        
1701        Cache<RequestModuleCacheKey, Set<Page>> pagesCache = _getRequestPageCache();
1702        
1703        // The site key in the cache is of the form {site + workspace}.
1704        String currentWorkspace = _workspaceSelector.getWorkspace();
1705        RequestModuleCacheKey pagesKey = RequestModuleCacheKey.of(project.getName(), workspaceModule.getId(), currentWorkspace);
1706        
1707        try
1708        {
1709            return pagesCache.get(pagesKey, __ -> _computePages(project, workspaceModule));
1710        }
1711        catch (CacheException e)
1712        {
1713            if (e.getCause() instanceof UnknownAmetysObjectException)
1714            {
1715                throw (UnknownAmetysObjectException) e.getCause();
1716            }
1717            else
1718            {
1719                throw new RuntimeException("An error occurred while computing page of module " + workspaceModule.getModuleName() + " in project " + project.getName(), e);
1720            }
1721        }        
1722    }
1723    
1724    private Set<Page> _computePages(Project project, WorkspaceModule workspaceModule)
1725    {
1726        Set<String> pagesUUids = _getMemoryPageCache().get(ModuleCacheKey.of(project.getName(), workspaceModule.getId()), __ -> _computePagesIds(project, workspaceModule));
1727        if (pagesUUids != null)
1728        {
1729            return pagesUUids.stream().map(uuid -> _resolver.<Page>resolveById(uuid)).collect(Collectors.toSet());
1730        }
1731        else
1732        {
1733            // Project may be present in cache for 'default' workspace but does not exist in current JCR workspace
1734            throw new UnknownAmetysObjectException("There is no pages for '" + project.getName() + "', module '" + workspaceModule.getModuleName() + "'");
1735        }
1736    }
1737    
1738    private Set<String> _computePagesIds(Project project, WorkspaceModule workspaceModule)
1739    {
1740        String siteName = Iterables.getFirst(getProjectNames(project), null);
1741        if (StringUtils.isEmpty(siteName))
1742        {
1743            return null;
1744        }
1745        
1746        ModifiableResourceCollection moduleRoot = workspaceModule.getModuleRoot(project, false);
1747        if (moduleRoot != null)
1748        {
1749            Expression expression = new StringExpression(__PAGE_MODULES_VALUE, Operator.EQ, moduleRoot.getId());
1750            String query = PageQueryHelper.getPageXPathQuery(siteName, null, null, expression, null);
1751    
1752            return StreamSupport.stream(_resolver.query(query).spliterator(), false)
1753                        .map(page -> page.getId())
1754                        .collect(Collectors.toSet());
1755        }
1756        else
1757        {
1758            return Set.of();
1759        }
1760    }
1761
1762    /**
1763     * Activate the list of module of the project
1764     * @param project The project
1765     * @param moduleIds The list of modules. Can be null to activate all modules 
1766     * @param additionalValues A list of optional additional values. Accepted values are : description, mailingList, inscriptionStatus, defaultProfile, tags, categoryTags, keywords and language
1767     */
1768    public void activateModules(Project project, Set<String> moduleIds, Map<String, Object> additionalValues)
1769    {
1770        Set<String> modules = moduleIds == null ? _moduleManagerEP.getExtensionsIds() : moduleIds;
1771        
1772        for (String moduleId : modules)
1773        {
1774            WorkspaceModule module = _moduleManagerEP.getModule(moduleId);
1775            if (module != null && !isModuleActivated(project, moduleId))
1776            {
1777                module.activateModule(project, additionalValues);
1778                project.addModule(moduleId);
1779            }
1780        }
1781        
1782        project.saveChanges();
1783    }    
1784    
1785    /**
1786     * Initialize the sitemap with the active module of the project
1787     * @param project The project
1788     * @param sitemap The sitemap
1789     */
1790    public void initializeModulesSitemap(Project project, Sitemap sitemap)
1791    {
1792        Set<String> modules = _moduleManagerEP.getExtensionsIds();
1793        
1794        for (String moduleId : modules)
1795        {
1796            if (_moduleManagerEP.hasExtension(moduleId))
1797            {
1798                WorkspaceModule module = _moduleManagerEP.getExtension(moduleId);
1799                
1800                if (isModuleActivated(project, moduleId))
1801                {
1802                    module.initializeSitemap(project, sitemap);
1803                }
1804            }
1805        }
1806    }
1807    
1808    /**
1809     * Determines if a module is activated
1810     * @param project The project
1811     * @param moduleId The id of module
1812     * @return true if the module the currently activated
1813     */
1814    public boolean isModuleActivated(Project project, String moduleId)
1815    {
1816        return ArrayUtils.contains(project.getModules(), moduleId);
1817    }
1818    
1819    /**
1820     * Remove the explorer root node of the project module, remove all events 
1821     * related to that module and set it to deactivated
1822     * @param project The project
1823     * @param moduleIds The id of module to activate
1824     */
1825    public void deactivateModules(Project project, Set<String> moduleIds)
1826    {
1827        for (String moduleId : moduleIds)
1828        {
1829            WorkspaceModule module = _moduleManagerEP.getModule(moduleId);
1830            if (module != null && isModuleActivated(project, moduleId))
1831            {
1832                module.deactivateModule(project);
1833                project.removeModule(moduleId);
1834            }
1835        }
1836        
1837        project.saveChanges();
1838    }
1839    
1840    
1841    /**
1842     * Get the list of profiles configured for the workspaces' projects
1843     * @return The list of profiles as JSON
1844     */
1845    @Callable
1846    public Map<String, Object> getProjectProfiles()
1847    {
1848        Map<String, Object> result = new HashMap<>();
1849        List<Map<String, Object>> profiles = _projectRightHelper.getProfiles().stream().map(p -> p.toJSON()).collect(Collectors.toList());
1850        result.put("profiles", profiles);
1851        return result;
1852    }
1853    
1854    /**
1855     * Get the tags from the projects
1856     * @param projectIds The ids of the projects
1857     * @return the tags of the projects
1858     */
1859    @Callable
1860    public Set<String> getTags(List<String> projectIds)
1861    {
1862        Set<String> tags = new HashSet<>();
1863        
1864        for (String projectId : projectIds)
1865        {
1866            Project project = _resolver.resolveById(projectId);
1867            tags.addAll(project.getTags());
1868        }
1869        
1870        return tags;
1871    }
1872    
1873    /**
1874     * Tag the projects
1875     * @param projectIds the project ids
1876     * @param tagNames the tag names
1877     * @param contextualParameters the contextuals parameters
1878     * @return results
1879     */
1880    @Callable
1881    public Map<String, Object> tag(List<String> projectIds, List<String> tagNames, Map<String, Object> contextualParameters)
1882    {
1883        return tag(projectIds, tagNames, TagMode.REPLACE.toString(), contextualParameters);
1884    }
1885    
1886    /**
1887     * Tag the projects
1888     * @param projectIds the project ids
1889     * @param tagNames the tag names
1890     * @param mode the mode The mode for updating tags: 'REPLACE' to replace tags, 'INSERT' to add tags or 'REMOVE' to remove tags.
1891     * @param contextualParameters the contextual parameters
1892     * @return results
1893     */
1894    @Callable
1895    public Map<String, Object> tag(List<String> projectIds, List<String> tagNames, String mode, Map<String, Object> contextualParameters)
1896    {
1897        Map<String, Object> result = new HashMap<>();
1898        
1899        result.put("invalid-tags", new ArrayList<String>());
1900        result.put("allright-projects", new ArrayList<Map<String, Object>>());
1901        
1902        for (String projectId : projectIds)
1903        {
1904            Project project = _resolver.resolveById(projectId);
1905            
1906            Map<String, Object> project2json = new HashMap<>();
1907            project2json.put("id", project.getId());
1908            project2json.put("title", project.getTitle());
1909            
1910            TagMode tagMode = TagMode.valueOf(mode);
1911            
1912            Set<String> oldTags = project.getTags();
1913            if (TagMode.REPLACE.equals(tagMode))
1914            {
1915                // First delete old tags
1916                for (String tagName : oldTags)
1917                {
1918                    project.untag(tagName);
1919                }
1920            }
1921            
1922            // Then set new tags
1923            for (String tagName : tagNames)
1924            {
1925                if (isTagValid(tagName))
1926                {
1927                    if (TagMode.REMOVE.equals(tagMode))
1928                    {
1929                        project.untag(tagName);
1930                    }
1931                    else if (TagMode.REPLACE.equals(tagMode) || !oldTags.contains(tagName))
1932                    {
1933                        project.tag(tagName);
1934                    }
1935                }
1936                else
1937                {
1938                    @SuppressWarnings("unchecked")
1939                    List<String> invalidTags = (List<String>) result.get("invalid-tags");
1940                    invalidTags.add(tagName);
1941                }
1942            }
1943            
1944            project.saveChanges();
1945            
1946            project2json.put("tags", project.getTags());
1947            @SuppressWarnings("unchecked")
1948            List<Map<String, Object>> allRightProjects = (List<Map<String, Object>>) result.get("allright-projects");
1949            allRightProjects.add(project2json);
1950            
1951            if (!oldTags.equals(project.getTags()))
1952            {
1953                // Notify observers that the project has been tagged
1954                Map<String, Object> eventParams = new HashMap<>();
1955                eventParams.put(ObservationConstants.ARGS_PROJECT, project);
1956                _observationManager.notify(new Event(ObservationConstants.EVENT_PROJECT_UPDATED, _currentUserProvider.getUser(), eventParams));
1957            }
1958        }
1959        
1960        return result;
1961    }
1962    
1963    /**
1964     * Test if a tag is valid
1965     * @param tagName The tag name
1966     * @return True if the tag is valid
1967     */
1968    public boolean isTagValid (String tagName)
1969    {
1970        Map<String, Object> params = new HashMap<>();
1971        Tag tag = _projectTagProviderEP.getTag(tagName, params);
1972        
1973        return tag != null;
1974    }
1975    
1976    /**
1977     * Get the site name holding the catalog of projects
1978     * @return the catalog's site name
1979     */
1980    public String getCatalogSiteName()
1981    {
1982        return Config.getInstance().getValue("workspaces.catalog.site.name");
1983    }
1984    
1985    /**
1986     * Get the site name holding the users directory
1987     * @return the users directory's site name
1988     */
1989    public String getUsersDirectorySiteName()
1990    {
1991        return Config.getInstance().getValue("workspaces.member.userdirectory.site.name");
1992    }
1993    
1994    @Override
1995    public boolean supports(Event event)
1996    {
1997        return event.getId().equals(ObservationConstants.EVENT_PROJECT_DELETED)
1998                || event.getId().equals(ObservationConstants.EVENT_PROJECT_UPDATED)
1999                || event.getId().equals(ObservationConstants.EVENT_PROJECT_ADDED)
2000                || event.getId().equals(org.ametys.web.ObservationConstants.EVENT_PAGE_ADDED)
2001                || event.getId().equals(org.ametys.web.ObservationConstants.EVENT_PAGE_DELETED);
2002    }
2003
2004    public int getPriority(Event event)
2005    {
2006        return 0;
2007    }
2008
2009    public void observe(Event event, Map<String, Object> transientVars) throws Exception
2010    {
2011        clearCaches();    
2012    }
2013
2014    /**
2015     * Prefix project title 
2016     * @param site the site
2017     * @param title the title
2018     */
2019    public void setProjectSiteTitle(Site site, String title)
2020    {
2021        I18nizableText i18nSiteTitle = new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_DEFAULT_PROJECT_WORKSPACE_TITLE", Arrays.asList(title));
2022        site.setTitle(_i18nUtils.translate(i18nSiteTitle));
2023    }
2024    
2025    private Project _computeProject(String projectName)
2026    {
2027        if (hasProject(projectName))
2028        {
2029            String uuid = _getUUIDCache().get(projectName);
2030            return _resolver.<Project>resolveById(uuid);
2031        }
2032        else
2033        {
2034            // Project may be present in cache for 'default' workspace but does not exist in current JCR workspace
2035            throw new UnknownAmetysObjectException("There is no site named '" + projectName + "'");
2036        }
2037    }
2038    
2039    /**
2040     * Check rights to create project
2041     * @param inscriptionStatus the inscription status
2042     */
2043    public void checkRightsForProjectCreation(InscriptionStatus inscriptionStatus)
2044    {
2045        switch (inscriptionStatus)
2046        {
2047            case PRIVATE:
2048                if (_rightManager.currentUserHasRight(ProjectConstants.RIGHT_PROJECT_CREATE_PRIVATE, "/${WorkspaceName}") != RightResult.RIGHT_ALLOW)
2049                {
2050                    throw new IllegalAccessError("Can't have rights to create private project");
2051                }
2052                break;
2053            case MODERATED:
2054                if (_rightManager.currentUserHasRight(ProjectConstants.RIGHT_PROJECT_CREATE_PUBLIC_MODERATED, "/${WorkspaceName}") != RightResult.RIGHT_ALLOW)
2055                {
2056                    throw new IllegalAccessError("Can't have rights to create public project with moderation");
2057                }
2058                break;
2059            case OPEN:
2060                if (_rightManager.currentUserHasRight(ProjectConstants.RIGHT_PROJECT_CREATE_PUBLIC_OPENED, "/${WorkspaceName}") != RightResult.RIGHT_ALLOW)
2061                {
2062                    throw new IllegalAccessError("Can't have rights to create public project");
2063                }
2064                break;
2065            default:
2066                throw new IllegalArgumentException("Inscription status '" + inscriptionStatus.toString() + "' is unknown");
2067        }
2068    }
2069    
2070    /**
2071     * Check rights to edit project
2072     * @param project the project
2073     * @param inscriptionStatus the inscription status
2074     */
2075    public void checkRightsForProjectEdition(Project project, InscriptionStatus inscriptionStatus)
2076    {
2077        InscriptionStatus oldInscriptionStatus = project.getInscriptionStatus();
2078        if (oldInscriptionStatus != inscriptionStatus)
2079        {
2080            boolean canCreatePrivateProjet = _rightManager.currentUserHasRight(ProjectConstants.RIGHT_PROJECT_CREATE_PRIVATE, "/${WorkspaceName}") == RightResult.RIGHT_ALLOW;
2081            boolean canCreatePublicProjetWithModeration = _rightManager.currentUserHasRight(ProjectConstants.RIGHT_PROJECT_CREATE_PUBLIC_MODERATED, "/${WorkspaceName}") == RightResult.RIGHT_ALLOW;
2082            boolean canCreatePublicProjet = _rightManager.currentUserHasRight(ProjectConstants.RIGHT_PROJECT_CREATE_PUBLIC_OPENED, "/${WorkspaceName}") == RightResult.RIGHT_ALLOW;
2083            
2084            switch (oldInscriptionStatus)
2085            {
2086                case PRIVATE:
2087                    if (!canCreatePrivateProjet)
2088                    {
2089                        throw new IllegalAccessError("Can't have rights to change the inscription status of private project");
2090                    }
2091                    break;
2092                case MODERATED:
2093                    if (!canCreatePublicProjetWithModeration)
2094                    {
2095                        throw new IllegalAccessError("Can't have rights to change the inscription status of public project with moderation");
2096                    }
2097                    break;
2098                case OPEN:
2099                    if (!canCreatePublicProjet)
2100                    {
2101                        throw new IllegalAccessError("Can't have rights to change the inscription status of public project");
2102                    }
2103                    break;
2104                default:
2105                    throw new IllegalArgumentException("Inscription status '" + oldInscriptionStatus.toString() + "' is unknown");
2106            }
2107            
2108            switch (inscriptionStatus)
2109            {
2110                case PRIVATE:
2111                    if (!canCreatePrivateProjet)
2112                    {
2113                        throw new IllegalAccessError("Can't have rights to change the project to private project");
2114                    }
2115                    break;
2116                case MODERATED:
2117                    if (!canCreatePublicProjetWithModeration)
2118                    {
2119                        throw new IllegalAccessError("Can't have rights to change the project to public project with moderation");
2120                    }
2121                    break;
2122                case OPEN:
2123                    if (!canCreatePublicProjet)
2124                    {
2125                        throw new IllegalAccessError("Can't have rights to change the project to public project");
2126                    }
2127                    break;
2128                default:
2129                    throw new IllegalArgumentException("Inscription status '" + inscriptionStatus.toString() + "' is unknown");
2130            }
2131        }
2132    }
2133    
2134    /**
2135     * Clear the site cache
2136     */
2137    public void clearCaches ()
2138    {
2139        _getMemorySiteAssociationCache().invalidateAll();
2140        _getMemoryProjectCache().invalidateAll();
2141        _getMemoryPageCache().invalidateAll();
2142        _getRequestProjectCache().invalidateAll();
2143        _getRequestPageCache().invalidateAll();
2144    }
2145    
2146    private Cache<String, List<Pair<String, String>>> _getMemorySiteAssociationCache() 
2147    {
2148        return _cacheManager.get(MEMORY_SITEASSOCIATION_CACHE);
2149    }
2150    
2151    private Cache<String, String> _getMemoryProjectCache()
2152    {
2153        return _cacheManager.get(MEMORY_PROJECTIDBYNAMECACHE);
2154    }
2155    
2156    private Cache<ModuleCacheKey, Set<String>> _getMemoryPageCache()
2157    {
2158        return _cacheManager.get(MEMORY_PAGESBYIDCACHE);
2159    }
2160    
2161    private Cache<RequestProjectCacheKey, Project> _getRequestProjectCache()
2162    {
2163        return _cacheManager.get(REQUEST_PROJECTBYID_CACHE);
2164    }
2165    
2166    private Cache<RequestModuleCacheKey, Set<Page>> _getRequestPageCache()
2167    {
2168        return _cacheManager.get(REQUEST_PAGESBYPROJECTANDMODULE_CACHE);
2169    }
2170
2171    
2172    /**
2173     * Creates the caches
2174     */
2175    protected void _createCaches()
2176    {
2177        _cacheManager.createMemoryCache(MEMORY_SITEASSOCIATION_CACHE, 
2178                new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_CACHE_PROJECT_MANAGER_LABEL"),
2179                new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_CACHE_PROJECT_MANAGER_DESCRIPTION"),
2180                true,
2181                null);
2182        _cacheManager.createMemoryCache(MEMORY_PROJECTIDBYNAMECACHE, 
2183                new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_PROJECT_MANAGER_UUID_CACHE_LABEL"),
2184                new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_PROJECT_MANAGER_UUID_CACHE_DESCRIPTION"),
2185                true,
2186                null);
2187        _cacheManager.createMemoryCache(MEMORY_PAGESBYIDCACHE, 
2188                new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_PROJECT_MANAGER_PAGEUUID_CACHE_LABEL"),
2189                new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_PROJECT_MANAGER_PAGEUUID_CACHE_DESCRIPTION"),
2190                true,
2191                null);
2192        _cacheManager.createRequestCache(REQUEST_PROJECTBYID_CACHE, 
2193                new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_PROJECT_MANAGER_REQUEST_CACHE_LABEL"),
2194                new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_PROJECT_MANAGER_REQUEST_CACHE_DESCRIPTION"),
2195                false);
2196        _cacheManager.createRequestCache(REQUEST_PAGESBYPROJECTANDMODULE_CACHE, 
2197                new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_PROJECT_MANAGER_PAGEREQUEST_CACHE_LABEL"),
2198                new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_PROJECT_MANAGER_PAGEREQUEST_CACHE_DESCRIPTION"),
2199                false);
2200    }
2201    
2202    private synchronized Map<String, String> _getUUIDCache()
2203    {
2204        if (!_getMemoryProjectCache().hasKey(__IS_CACHE_FILLED))
2205        {
2206            Session defaultSession = null;
2207            try
2208            {
2209                // Force default workspace to execute query
2210                defaultSession = _repository.login(RepositoryConstants.DEFAULT_WORKSPACE);
2211                
2212                String jcrQuery = "//element(*, ametys:project)";
2213                
2214                AmetysObjectIterable<Project> projects = _resolver.query(jcrQuery, defaultSession);
2215                
2216                for (Project project : projects)
2217                {
2218                    _getMemoryProjectCache().put(project.getName(), project.getId());
2219                }
2220                
2221                _getMemoryProjectCache().put(__IS_CACHE_FILLED, null);
2222            }
2223            catch (RepositoryException e)
2224            {
2225                throw new AmetysRepositoryException(e);
2226            }
2227            finally
2228            {
2229                if (defaultSession != null)
2230                {
2231                    defaultSession.logout();
2232                }
2233            }
2234        }
2235        
2236        Map<String, String> cacheAsMap = _getMemoryProjectCache().asMap();
2237        cacheAsMap.remove(__IS_CACHE_FILLED);
2238        return cacheAsMap;
2239    }
2240    
2241    private static final class RequestProjectCacheKey extends AbstractCacheKey
2242    {
2243        private RequestProjectCacheKey(String projectName, String workspaceName)
2244        {
2245            super(projectName, workspaceName);
2246        }
2247        
2248        static RequestProjectCacheKey of(String projectName, String workspaceName)
2249        {
2250            return new RequestProjectCacheKey(projectName, workspaceName);
2251        }
2252    }
2253    
2254    private static final class ModuleCacheKey extends AbstractCacheKey
2255    {
2256        private ModuleCacheKey(String projectName, String moduleId)
2257        {
2258            super(projectName, moduleId);
2259        }
2260        
2261        static ModuleCacheKey of(String projectName, String moduleId)
2262        {
2263            return new ModuleCacheKey(projectName, moduleId);
2264        }
2265    }
2266    
2267    private static final class RequestModuleCacheKey extends AbstractCacheKey
2268    {
2269        private RequestModuleCacheKey(String projectName, String moduleId, String workspaceName)
2270        {
2271            super(projectName, moduleId, workspaceName);
2272        }
2273        
2274        static RequestModuleCacheKey of(String projectName, String moduleId, String workspaceName)
2275        {
2276            return new RequestModuleCacheKey(projectName, moduleId, workspaceName);
2277        }
2278    }
2279    
2280    private Request _getRequest ()
2281    {
2282        try 
2283        {
2284            return (Request) _context.get(ContextHelper.CONTEXT_REQUEST_OBJECT);
2285        } 
2286        catch (ContextException ce)
2287        {
2288            getLogger().info("Unable to get the request", ce);
2289            return null;
2290        }
2291    }
2292}