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