001/*
002 *  Copyright 2016 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.members;
017
018import java.util.ArrayList;
019import java.util.Arrays;
020import java.util.Collection;
021import java.util.Collections;
022import java.util.Comparator;
023import java.util.HashMap;
024import java.util.HashSet;
025import java.util.List;
026import java.util.Map;
027import java.util.Objects;
028import java.util.Optional;
029import java.util.Set;
030import java.util.function.BiPredicate;
031import java.util.function.Predicate;
032import java.util.stream.Collectors;
033import java.util.stream.Stream;
034
035import org.apache.avalon.framework.component.Component;
036import org.apache.avalon.framework.context.Context;
037import org.apache.avalon.framework.context.ContextException;
038import org.apache.avalon.framework.context.Contextualizable;
039import org.apache.avalon.framework.service.ServiceException;
040import org.apache.avalon.framework.service.ServiceManager;
041import org.apache.avalon.framework.service.Serviceable;
042import org.apache.cocoon.components.ContextHelper;
043import org.apache.cocoon.environment.Request;
044import org.apache.commons.collections.CollectionUtils;
045import org.apache.commons.lang3.StringUtils;
046import org.apache.http.annotation.Obsolete;
047
048import org.ametys.cms.repository.Content;
049import org.ametys.core.group.Group;
050import org.ametys.core.group.GroupIdentity;
051import org.ametys.core.group.GroupManager;
052import org.ametys.core.observation.Event;
053import org.ametys.core.observation.ObservationManager;
054import org.ametys.core.right.ProfileAssignmentStorageExtensionPoint;
055import org.ametys.core.right.RightManager;
056import org.ametys.core.right.RightProfilesDAO;
057import org.ametys.core.ui.Callable;
058import org.ametys.core.user.CurrentUserProvider;
059import org.ametys.core.user.User;
060import org.ametys.core.user.UserIdentity;
061import org.ametys.core.user.UserManager;
062import org.ametys.plugins.core.user.UserHelper;
063import org.ametys.plugins.explorer.resources.ModifiableResourceCollection;
064import org.ametys.plugins.repository.AmetysObject;
065import org.ametys.plugins.repository.AmetysObjectIterable;
066import org.ametys.plugins.repository.AmetysObjectResolver;
067import org.ametys.plugins.repository.AmetysRepositoryException;
068import org.ametys.plugins.repository.ModifiableAmetysObject;
069import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
070import org.ametys.plugins.repository.RepositoryConstants;
071import org.ametys.plugins.userdirectory.UserDirectoryHelper;
072import org.ametys.plugins.workspaces.ObservationConstants;
073import org.ametys.plugins.workspaces.members.JCRProjectMember.MemberType;
074import org.ametys.plugins.workspaces.project.ProjectManager;
075import org.ametys.plugins.workspaces.project.modules.WorkspaceModule;
076import org.ametys.plugins.workspaces.project.modules.WorkspaceModuleExtensionPoint;
077import org.ametys.plugins.workspaces.project.objects.Project;
078import org.ametys.plugins.workspaces.project.objects.Project.InscriptionStatus;
079import org.ametys.plugins.workspaces.project.rights.ProjectRightHelper;
080import org.ametys.runtime.plugin.component.AbstractLogEnabled;
081import org.ametys.web.population.PopulationContextHelper;
082import org.ametys.web.repository.page.ModifiablePage;
083import org.ametys.web.repository.page.Page;
084import org.ametys.web.repository.site.Site;
085
086import com.google.common.collect.ImmutableList;
087
088/**
089 * Helper component for managing project's users
090 */
091public class ProjectMemberManager extends AbstractLogEnabled implements Serviceable, Component, Contextualizable
092{
093    /** Avalon Role */
094    public static final String ROLE = ProjectMemberManager.class.getName();
095    
096    /** The id of the members service */
097    public static final String __WORKSPACES_SERVICE_MEMBERS = "org.ametys.plugins.workspaces.module.Members";
098    
099    @Obsolete // For v1 project only
100    private static final String __PROJECT_RIGHT_PROFILE = "PROJECT";
101    
102    /** Constants for users project node */
103    private static final String __PROJECT_MEMBERS_NODE = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":members";
104    
105    /** The type of the project users node type */
106    private static final String __PROJECT_MEMBERS_NODE_TYPE = RepositoryConstants.NAMESPACE_PREFIX + ":unstructured";
107
108    /** The type of a project user node type */
109    private static final String __PROJECT_MEMBER_NODE_TYPE = RepositoryConstants.NAMESPACE_PREFIX + ":project-member";
110 
111    /** Avalon context */
112    protected Context _context;
113    
114    /** Project manager */
115    protected ProjectManager _projectManager;
116
117    /** Project rights helper */
118    protected ProjectRightHelper _projectRightHelper;
119
120    /** Profiles right manager */
121    protected RightProfilesDAO _rightProfilesDAO;
122
123    /** Profile assignment storage */
124    protected ProfileAssignmentStorageExtensionPoint _profileAssignmentStorageExtensionPoint;
125    
126    /** Ametys object resolver */
127    protected AmetysObjectResolver _resolver;
128
129    /** Rights manager */
130    protected RightManager _rightManager;
131
132    /** Current user provider */
133    protected CurrentUserProvider _currentUserProvider;
134
135    /** Users manager */
136    protected UserManager _userManager;
137
138    /** The observation manager */
139    protected ObservationManager _observationManager;
140
141    /** Module managers EP */
142    protected WorkspaceModuleExtensionPoint _moduleManagerEP;
143    
144    /** The user helper */
145    protected UserHelper _userHelper;
146
147    /** The groups manager */
148    protected GroupManager _groupManager;
149
150    /** The population context helper */
151    protected PopulationContextHelper _populationContextHelper;
152
153    /** The user directory helper */
154    protected UserDirectoryHelper _userDirectoryHelper;
155
156    @Override
157    public void contextualize(Context context) throws ContextException
158    {
159        _context = context;
160    }
161    
162    @Override
163    public void service(ServiceManager manager) throws ServiceException
164    {
165        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
166        _projectManager = (ProjectManager) manager.lookup(ProjectManager.ROLE);
167        _projectRightHelper = (ProjectRightHelper) manager.lookup(ProjectRightHelper.ROLE);
168        _rightProfilesDAO = (RightProfilesDAO) manager.lookup(RightProfilesDAO.ROLE);
169        _profileAssignmentStorageExtensionPoint = (ProfileAssignmentStorageExtensionPoint) manager.lookup(ProfileAssignmentStorageExtensionPoint.ROLE);
170        _rightManager = (RightManager) manager.lookup(RightManager.ROLE);
171        _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
172        _userManager = (UserManager) manager.lookup(UserManager.ROLE);
173        _groupManager = (GroupManager) manager.lookup(GroupManager.ROLE);
174        _userHelper = (UserHelper) manager.lookup(UserHelper.ROLE);
175        _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE);
176        _moduleManagerEP = (WorkspaceModuleExtensionPoint) manager.lookup(WorkspaceModuleExtensionPoint.ROLE);
177        _populationContextHelper = (PopulationContextHelper) manager.lookup(org.ametys.core.user.population.PopulationContextHelper.ROLE);
178        _userDirectoryHelper = (UserDirectoryHelper) manager.lookup(UserDirectoryHelper.ROLE);
179    }
180    
181    /**
182     * Retrieve the data of a member of a project, or the default data if no user is provided
183     * @param projectName The name of the project
184     * @param identity The user or group identity. If null, return the default profiles for a new user
185     * @param type The type of the identity. Can be "user" or "group"
186     * @return The map of profiles per module for the user
187     * @throws IllegalAccessException If the user cannot execute this operation 
188     */
189    @Callable
190    public Map<String, Object> getProjectMemberData(String projectName, String identity, String type) throws IllegalAccessException
191    {
192        Map<String, Object> result = new HashMap<>();
193        
194        boolean isTypeUser = JCRProjectMember.MemberType.USER.toString().equals(type);
195        boolean isTypeGroup = JCRProjectMember.MemberType.GROUP.toString().equals(type);
196        UserIdentity user = Optional.ofNullable(identity)
197                                    .filter(id -> id != null && isTypeUser)
198                                    .map(UserIdentity::stringToUserIdentity)
199                                    .orElse(null);
200        GroupIdentity group = Optional.ofNullable(identity)
201                                      .filter(id -> id != null && isTypeGroup)
202                                      .map(GroupIdentity::stringToGroupIdentity)
203                                      .orElse(null);
204        
205        if (identity != null)
206        {
207            if (isTypeGroup && group == null)
208            {
209                result.put("message", "unknow-group");
210                return result;
211            }
212            else if (isTypeUser && user == null)
213            {
214                result.put("message", "unknow-user");
215                return result;
216            }
217        }
218        
219        Project project = _projectManager.getProject(projectName);
220        if (project == null)
221        {
222            result.put("message", "unknow-project");
223            return result;
224        }
225        
226        if (!_projectRightHelper.canAddMember(project))
227        {
228            throw new IllegalAccessException("User '" + _currentUserProvider.getUser() + "' tried to get member's rights without convenient right [" + projectName + ", " + identity + "]");
229        }
230        
231        boolean newMember = true;
232        Map<String, String> userProfiles;
233        
234        if (user != null || group != null)
235        {
236            JCRProjectMember projectMember = user != null ? getOrCreateProjectMember(project, user) : getOrCreateProjectMember(project, group);
237            
238            newMember = projectMember.needsSave();
239            
240            String role = projectMember.getRole();
241            if (role != null)
242            {
243                result.put("role", role);
244            }
245            
246            userProfiles = _getMemberProfiles(projectMember, project);
247        }
248        else
249        {
250            userProfiles = new HashMap<>();
251        }
252        
253        result.put("profiles", userProfiles);
254        result.put("status", newMember ? "new" : "edit");
255        
256        return result;
257    }
258
259    private Map<String, String> _getMemberProfiles(JCRProjectMember member, Project project)
260    {
261        Map<String, String> userProfiles = new HashMap<>();
262        
263        // Get allowed profile on modules (among the project members's profiles)
264        for (WorkspaceModule module : _projectManager.getModules(project))
265        {
266            String allowedProfileOnProject = _projectRightHelper.getAllowedProfileOnModule(project, module, member);
267            userProfiles.put(module.getId(), allowedProfileOnProject);
268        }
269        
270        return userProfiles;
271    }
272    
273    /**
274     * Set the user data in the project
275     * @param projectName The project name
276     * @param identity The user or group identity.
277     * @param type The type of the identity. Can be "user" or "group"
278     * @param newProfiles The profiles to affect, mapped by module
279     * @param role The user role inside the project
280     * @return The result
281     * @throws IllegalAccessException If the user cannot execute this operation
282     */
283    @Callable
284    public Map<String, Object> setProjectMemberData(String projectName, String identity, String type, Map<String, String> newProfiles, String role) throws IllegalAccessException
285    {
286        Map<String, Object> result = new HashMap<>();
287        
288        boolean isTypeUser = JCRProjectMember.MemberType.USER.toString().equals(type);
289        boolean isTypeGroup = JCRProjectMember.MemberType.GROUP.toString().equals(type);
290        UserIdentity user = Optional.ofNullable(identity)
291                                    .filter(id -> id != null && isTypeUser)
292                                    .map(UserIdentity::stringToUserIdentity)
293                                    .orElse(null);
294        GroupIdentity group = Optional.ofNullable(identity)
295                                      .filter(id -> id != null && isTypeGroup)
296                                      .map(GroupIdentity::stringToGroupIdentity)
297                                      .orElse(null);
298        
299        if (group == null && user == null)
300        {
301            result.put("message", isTypeGroup ? "unknow-group" : "unknow-user");
302            return result;
303        }
304        
305        
306        Project project = _projectManager.getProject(projectName);
307        if (project == null)
308        {
309            result.put("message", "unknow-project");
310            return result;
311        }
312        
313        if (!_projectRightHelper.canAddMember(project))
314        {
315            throw new IllegalAccessException("User '" + _currentUserProvider.getUser() + "' tried to set member rights without convenient right [" + projectName + ", " + identity + "]");
316        }
317        
318        JCRProjectMember projectMember = isTypeUser ? getOrCreateProjectMember(project, user) : getOrCreateProjectMember(project, group);
319        boolean newMember = projectMember.needsSave();
320        
321        if (role != null)
322        {
323            projectMember.setRole(role);
324        }
325        
326        setMemberProfiles(newProfiles, projectMember, project, newMember);
327        
328        project.saveChanges();
329        
330        if (newMember)
331        {
332            // Notify listeners
333            Map<String, Object> eventParams = new HashMap<>();
334            eventParams.put(ObservationConstants.ARGS_MEMBER, projectMember);
335            eventParams.put(ObservationConstants.ARGS_MEMBER_ID, projectMember.getId());
336            eventParams.put(ObservationConstants.ARGS_PROJECT, project);
337            eventParams.put(ObservationConstants.ARGS_PROJECT_ID, project.getId());
338            _observationManager.notify(new Event(ObservationConstants.EVENT_MEMBER_ADDED, _currentUserProvider.getUser(), eventParams));
339        }
340        
341        return result;
342    }
343    
344    /**
345     * Add a user to a project with open inscriptions, using the default values
346     * @param project The project
347     * @param user The user
348     * @return True if the user was successfully added
349     */
350    public boolean addProjectMember(Project project, UserIdentity user)
351    {
352        if (user == null)
353        {
354            return false;
355        }
356        
357        InscriptionStatus inscriptionStatus = project.getInscriptionStatus();
358        if (!inscriptionStatus.equals(InscriptionStatus.OPEN))
359        {
360            return false;
361        }
362        
363        JCRProjectMember projectMember = getOrCreateProjectMember(project, user);
364        
365        // Allow the user to access the project's site
366        _allowReadAccessOnProject(projectMember, project);
367        
368        Set<String> allowedProfiles = Stream.of(RightManager.READER_PROFILE_ID, project.getDefaultProfile())
369                .filter(Objects::nonNull)
370                .collect(Collectors.toSet());
371        
372        for (WorkspaceModule module : _moduleManagerEP.getModules())
373        {
374            _setProfileOnModule(projectMember, project, module, allowedProfiles);
375        }
376        
377        project.saveChanges();
378        
379        // Notify listeners
380        Map<String, Object> eventParams = new HashMap<>();
381        eventParams.put(ObservationConstants.ARGS_MEMBER, projectMember);
382        eventParams.put(ObservationConstants.ARGS_MEMBER_ID, projectMember.getId());
383        eventParams.put(ObservationConstants.ARGS_PROJECT, project);
384        eventParams.put(ObservationConstants.ARGS_PROJECT_ID, project.getId());
385        _observationManager.notify(new Event(ObservationConstants.EVENT_MEMBER_ADDED, _currentUserProvider.getUser(), eventParams));
386        
387        return true;
388    }
389
390    /**
391     * Set the profiles for a member
392     * @param newProfiles The allowed profile by module
393     * @param projectMember The member
394     * @param project The project
395     * @param newMember true if it is a new created member
396     */
397    public void setMemberProfiles(Map<String, String> newProfiles, JCRProjectMember projectMember, Project project, boolean newMember)
398    {
399        // Allow the user to access the project's site
400        _allowReadAccessOnProject(projectMember, project);
401        
402        for (Map.Entry<String, String> entry : newProfiles.entrySet())
403        {
404            String moduleId = entry.getKey();
405            String profileId = entry.getValue();
406            
407            if (__PROJECT_RIGHT_PROFILE.equals(moduleId))
408            {
409                // V1 project only
410                if (profileId != null)
411                {
412                    _addProfile(projectMember, profileId, project);
413                }
414                else
415                {
416                    _removeProfile(projectMember, profileId, project);
417                }
418            }
419            else
420            {
421                Set<String> allowedProfiles = profileId != null ? Stream.of(RightManager.READER_PROFILE_ID, profileId).collect(Collectors.toSet()) : Collections.EMPTY_SET;
422                
423                WorkspaceModule module = _moduleManagerEP.getModule(moduleId);
424                _setProfileOnModule(projectMember, project, module, allowedProfiles);
425            }
426        }
427    }
428    
429    private void _setProfileOnModule(JCRProjectMember member, Project project, WorkspaceModule module, Set<String> allowedProfiles)
430    {
431        if (module != null && _projectManager.isModuleActivated(project, module.getId()))
432        {
433            AmetysObject moduleObject = module.getModuleRoot(project, false);
434            _setMemberProfiles(member, allowedProfiles, moduleObject);
435            
436            // Update read access on project module's pages
437            AmetysObjectIterable<Page> modulePages = module.getModulePages(project, null);
438            for (Page modulePage : modulePages)
439            {
440                _setMemberProfiles(member, allowedProfiles.contains(RightManager.READER_PROFILE_ID) ? Collections.singleton(RightManager.READER_PROFILE_ID) : Collections.EMPTY_SET, modulePage);
441            }
442        }
443    }
444    
445    private Set<String> _getAllowedProfile(JCRProjectMember member, AmetysObject object)
446    {
447        return MemberType.GROUP.toString().equals(member.getType()) 
448                ? _profileAssignmentStorageExtensionPoint.getAllowedProfilesForGroup(object, member.getGroup()) 
449                : _profileAssignmentStorageExtensionPoint.getAllowedProfilesForUser(object, member.getUser());
450    }
451
452    private void _setMemberProfiles(JCRProjectMember member, Set<String> allowedProfiles, AmetysObject object)
453    {
454        Set<String> currentAllowedProfiles = _getAllowedProfile(member, object);
455        
456        Collection<String> profilesToRemove  = CollectionUtils.removeAll(currentAllowedProfiles, allowedProfiles);
457        
458        Collection<String> profilesToAdd  = CollectionUtils.removeAll(allowedProfiles, currentAllowedProfiles);
459        
460        for (String profileId : profilesToRemove)
461        {
462            _removeProfile(member, profileId, object);
463        }
464        
465        for (String profileId : profilesToAdd)
466        {
467            _addProfile(member, profileId, object);
468        }
469        
470        Collection<String> updatedProfiles = CollectionUtils.union(profilesToAdd, profilesToRemove);
471        
472        if (updatedProfiles.size() > 0)
473        {
474            Map<String, Object> eventParams = new HashMap<>();
475            eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_CONTEXT, object);
476            eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_CONTEXT_IDENTIFIER, object.getId());
477            eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_PROFILES, updatedProfiles);
478            eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_SOLR_CACHE_UNINFLUENTIAL, true);
479            
480            _observationManager.notify(new Event(org.ametys.core.ObservationConstants.EVENT_ACL_UPDATED, _currentUserProvider.getUser(), eventParams));
481        }
482    }
483    
484    private void _removeProfile(JCRProjectMember member, String profileId, AmetysObject aclObject)
485    {
486        if (MemberType.GROUP.toString().equals(member.getType()))
487        {
488            _profileAssignmentStorageExtensionPoint.removeAllowedProfileFromGroup(member.getGroup(), profileId, aclObject);
489        }
490        else
491        {
492            _profileAssignmentStorageExtensionPoint.removeAllowedProfileFromUser(member.getUser(), profileId, aclObject);
493        }
494    }
495    
496    private void _addProfile(JCRProjectMember member, String profileId, AmetysObject aclObject)
497    {
498        if (MemberType.GROUP.toString().equals(member.getType()))
499        {
500            _profileAssignmentStorageExtensionPoint.allowProfileToGroup(member.getGroup(), profileId, aclObject);
501        }
502        else
503        {
504            _profileAssignmentStorageExtensionPoint.allowProfileToUser(member.getUser(), profileId, aclObject);
505        }
506    }
507
508    private void _setReadAccess(JCRProjectMember member, AmetysObject aclObject)
509    {
510        Set<String> currentAllowedProfiles = _getAllowedProfile(member, aclObject);
511                
512        if (!currentAllowedProfiles.contains(RightManager.READER_PROFILE_ID))
513        {
514            _addProfile(member, RightManager.READER_PROFILE_ID, aclObject);
515            
516            Map<String, Object> eventParams = new HashMap<>();
517            eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_CONTEXT, aclObject);
518            eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_CONTEXT_IDENTIFIER, aclObject.getId());
519            eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_PROFILES, Collections.singleton(RightManager.READER_PROFILE_ID));
520            eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_SOLR_CACHE_UNINFLUENTIAL, true);
521            
522            _observationManager.notify(new Event(org.ametys.core.ObservationConstants.EVENT_ACL_UPDATED, MemberType.GROUP.toString().equals(member.getType()) ? null : member.getUser(), eventParams));
523        }
524    }
525    
526    private void _allowReadAccessOnProject(JCRProjectMember member, Project project)
527    {
528        _setReadAccess(member, project);
529        
530        // Add READER profile for all dashboards of project
531        for (ModifiablePage dashboardPage : _getProjectDashboardPages(project))
532        {
533            _setReadAccess(member, dashboardPage);
534        }
535    }
536    
537    private List<ModifiablePage> _getProjectDashboardPages(Project project)
538    {
539        List<ModifiablePage> dashboardPages = project.getSites()
540                                                     .stream()
541                                                     .map(Site::getSitemaps)
542                                                     .flatMap(AmetysObjectIterable::stream)
543                                                     .map(sitemap -> _projectManager.getProjectDashboardPage(project, sitemap.getSitemapName()))
544                                                     .flatMap(AmetysObjectIterable::stream)
545                                                     .filter(page -> page instanceof ModifiablePage)
546                                                     .map(ModifiablePage.class::cast)
547                                                     .collect(Collectors.toList());
548        return dashboardPages;
549    }
550    
551    private void _removeMemberProfiles(JCRProjectMember member, AmetysObject object)
552    {
553        Set<String> currentAllowedProfiles = _getAllowedProfile(member, object);
554        
555        for (String allowedProfile : currentAllowedProfiles)
556        {
557            _removeProfile(member, allowedProfile, object);
558        }
559        
560        if (currentAllowedProfiles.size() > 0)
561        {
562            ((ModifiableAmetysObject) object).saveChanges();
563            
564            Map<String, Object> eventParams = new HashMap<>();
565            eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_CONTEXT, object);
566            eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_CONTEXT_IDENTIFIER, object.getId());
567            eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_PROFILES, currentAllowedProfiles);
568            eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_SOLR_CACHE_UNINFLUENTIAL, true);
569            
570            _observationManager.notify(new Event(org.ametys.core.ObservationConstants.EVENT_ACL_UPDATED, _currentUserProvider.getUser(), eventParams));
571        }
572    }
573    
574    /**
575     * Get the current user information
576     * @return The user
577     */
578    @Callable
579    public Map<String, Object> getCurrentUser()
580    {
581        Map<String, Object> result = new HashMap<>();
582        result.put("user", _userHelper.user2json(_currentUserProvider.getUser()));
583        return result;
584    }
585    
586    /**
587     * Get the members of current project or all the members of all projects in where is no current project
588     * @return The members
589     */
590    @Callable
591    public Map<String, Object> getProjectMembers()
592    {
593        Map<String, Object> result = new HashMap<>();
594        
595        Request request = ContextHelper.getRequest(_context);
596        String projectName = (String) request.getAttribute("projectName");
597        
598        Collection<Project> projects = new ArrayList<>();
599        
600        if (StringUtils.isNotEmpty(projectName))
601        {
602            projects.add(_projectManager.getProject(projectName));
603        }
604        else
605        {
606            _projectManager.getProjects()
607                           .stream()
608                           .forEach(project -> projects.add(project));
609        }
610        
611        Set<String> projectsPopulations = projects.stream()
612                                                  .map(project -> project.getSites())
613                                                  .flatMap(Collection::stream)
614                                                  .map(site -> _populationContextHelper.getUserPopulationsOnContext("/sites/" + site.getName(), false))
615                                                  .flatMap(Set::stream)
616                                                  .collect(Collectors.toSet());
617        
618        Set<JCRProjectMember> members = projects.stream()
619                .map(project -> getProjectMembers(project))
620                                                .flatMap(Set::stream)
621                                                .collect(Collectors.toSet());
622        
623        Set<UserIdentity> users = members.stream()
624                                         .filter(member -> MemberType.USER.toString().equals(member.getType()))
625                                         .map(JCRProjectMember::getUser)
626                                         .collect(Collectors.toSet());
627 
628        users.addAll(members.stream()
629                            .filter(member -> MemberType.GROUP.toString().equals(member.getType()))
630                            .map(member -> _groupManager.getGroup(member.getGroup()))
631                            .filter(Objects::nonNull)
632                            .map(group -> group.getUsers())
633                            .flatMap(Set::stream)
634                            .filter(user -> projectsPopulations.contains(user.getPopulationId()))
635                            .collect(Collectors.toSet()));
636        
637        result.put("users", users.stream()
638                                 .map(identity -> _userHelper.getUser(identity))
639                                 .filter(Objects::nonNull)
640                                 .map(user -> _userHelper.user2json(user, true))
641                                 .collect(Collectors.toList()));
642        
643        return result;
644    }
645    
646    /**
647     * Get the members of a project, sorted by managers, non empty role and name
648     * @param projectName the project's name
649     * @param lang the language to get user content
650     * @param expandGroup true to expand the user of a group
651     * @param withUserContent true to get the user content
652     * @return the members of project
653     * @throws IllegalAccessException if an error occurred
654     * @throws AmetysRepositoryException if an error occurred
655     */
656    @Callable
657    public List<Map<String, Object>> getProjectMembers(String projectName, String lang, boolean expandGroup, boolean withUserContent) throws IllegalAccessException, AmetysRepositoryException
658    {
659        Map<String, Object> result = new HashMap<>();
660        
661        Project project = _projectManager.getProject(projectName);
662        if (!_projectRightHelper.hasReadAccess(project))
663        {
664            throw new IllegalAccessException("User '" + _currentUserProvider.getUser() + "' tried to access a privilege feature without reader right in the project " + project.getPath());
665        }
666        
667        List<Map<String, Object>> membersData = new ArrayList<>();
668        
669        List<ProjectMember> projectMembers = getProjectMembers(project, expandGroup);
670        
671        for (ProjectMember projectMember : projectMembers)
672        {
673            Map<String, Object> memberData = new HashMap<>();
674            
675            memberData.put("type", projectMember.getType().toString());
676            memberData.put("title", projectMember.getTitle());
677            memberData.put("sortabletitle", projectMember.getSortableTitle());
678            memberData.put("manager", projectMember.isManager());
679            
680            String role = projectMember.getRole();
681            if (StringUtils.isNotEmpty(role))
682            {
683                memberData.put("role", role);
684            }
685            
686            User user = projectMember.getUser();
687            if (user != null)
688            {
689                memberData.put("id", UserIdentity.userIdentityToString(user.getIdentity()));
690                memberData.putAll(_userHelper.user2json(user));
691
692                if (withUserContent)
693                {
694                    Content userContent = _userDirectoryHelper.getUserContent(user.getIdentity(), lang);
695                    if (userContent != null)
696                    {
697                        memberData.put("userContentId", userContent.getId());
698                    }
699                }
700            }
701            
702            Group group = projectMember.getGroup();
703            if (group != null)
704            {
705                memberData.put("id", GroupIdentity.groupIdentityToString(group.getIdentity()));
706                memberData.putAll(group2Json(group));
707            }
708            
709            membersData.add(memberData);
710        }
711        
712        result.put("members", membersData);
713        
714        return membersData;
715    }
716    
717    /**
718     * Get the members of a project, sorted by managers, non empty role and name
719     * @param project the project
720     * @param expandGroup true to expand the user of a group
721     * @return the members of project
722     * @throws IllegalAccessException if an error occurred
723     * @throws AmetysRepositoryException if an error occurred
724     */
725    public List<ProjectMember> getProjectMembers(Project project, boolean expandGroup) throws IllegalAccessException, AmetysRepositoryException
726    {
727        Set<ProjectMember> members = new HashSet<>(); // avoid duplication
728        
729        Set<JCRProjectMember> jcrMembers = getProjectMembers(project);
730        
731        List<UserIdentity> managers = Arrays.asList(project.getManagers());
732        
733        for (JCRProjectMember jcrMember : jcrMembers)
734        {
735            if (MemberType.USER.toString().equals(jcrMember.getType()))
736            {
737                UserIdentity userIdentity = jcrMember.getUser();
738                User user = _userManager.getUser(userIdentity);
739                if (user != null)
740                {
741                    boolean isManager = managers.contains(userIdentity);
742                    
743                    ProjectMember projectMember = new ProjectMember(user.getFullName(), user.getSortableName(), user, jcrMember.getRole(), isManager);
744                    if (!members.add(projectMember)) 
745                    {
746                        //if set already contains the user, override it (users always take over users' group)
747                        members.remove(projectMember); // remove the one in  the set
748                        members.add(projectMember); // add the new one
749                    }
750                }
751            }
752            else if (MemberType.GROUP.toString().equals(jcrMember.getType()))
753            {
754                GroupIdentity groupIdentity = jcrMember.getGroup();
755                Group group = _groupManager.getGroup(groupIdentity);
756                if (group != null)
757                {
758                    if (expandGroup)
759                    {
760                        for (UserIdentity userIdentity : group.getUsers())
761                        {
762                            User user = _userManager.getUser(userIdentity);
763                            if (user != null)
764                            {
765                                ProjectMember projectMember = new ProjectMember(user.getFullName(), user.getSortableName(), user, null, false);
766                                members.add(projectMember); // add if does not exist yet
767                            }
768                        }
769                    }
770                    else
771                    {
772                        // Add the member as group
773                        members.add(new ProjectMember(group.getLabel(), group.getLabel(), group));
774                    }
775                }
776            }
777        }
778
779        // Sort by managers, not empty role then alpha order
780        List<ProjectMember> membersAsList = new ArrayList<>(members);
781
782        Comparator<ProjectMember> managerComparator = Comparator.comparing(m -> m.isManager() ? 0 : 1);
783        Comparator<ProjectMember> roleComparator = Comparator.comparing(m -> StringUtils.isNotBlank(m.getRole()) ? 0 : 1);
784        Comparator<ProjectMember> nameComparator = (m1, m2) -> m1.getSortableTitle().compareToIgnoreCase(m2.getSortableTitle());
785        Collections.sort(membersAsList, managerComparator.thenComparing(roleComparator).thenComparing(nameComparator));
786        
787        return membersAsList;
788    }
789    
790    /**
791     * Retrieves the rights for the current user in the project
792     * @param projectName The project Name
793     * @return The project
794     */
795    @Callable
796    public Map<String, Object> getMemberModuleRights(String projectName)
797    {
798        Map<String, Object> rights = new HashMap<>();
799        
800        Project project = _projectManager.getProject(projectName);
801        if (project == null)
802        {
803            rights.put("message", "unknow-project");
804            return rights;
805        }
806        rights.put("view", _projectRightHelper.canViewMembers(project));
807        rights.put("add", _projectRightHelper.canAddMember(project));
808        rights.put("edit", _projectRightHelper.canAddMember(project));
809        rights.put("delete", _projectRightHelper.canRemoveMember(project));
810        
811        return rights;
812    }
813    
814    /**
815     * Get the list of users of the project
816     * @param project The project
817     * @return The list of users
818     */
819    public Set<JCRProjectMember> getProjectMembers(Project project)
820    {
821        Set<JCRProjectMember> projectUsers = new HashSet<>();
822        
823        if (project != null)
824        {
825            ModifiableTraversableAmetysObject usersNode = _getProjectMembersNode(project);
826            
827            for (AmetysObject userNode : usersNode.getChildren())
828            {
829                if (userNode instanceof JCRProjectMember)
830                {
831                    projectUsers.add((JCRProjectMember) userNode);
832                }
833            }
834        }
835        
836        return projectUsers;
837    }
838    
839    /**
840     * Test if an user is a member of a project
841     * @param project The project
842     * @param userIdentity The user identity
843     * @return True if this user is a member of this project
844     */
845    public boolean isProjectMember(Project project, UserIdentity userIdentity)
846    {
847        return getProjectMember(project, userIdentity) != null;
848    }
849    
850    /**
851     * Retrieve the member of a project corresponding to the user identity
852     * @param project The project
853     * @param userIdentity The user identity
854     * @return The member of this project, which can be of type "user" or "group", or null if the user is not in the project
855     */
856    public JCRProjectMember getProjectMember(Project project, UserIdentity userIdentity)
857    {
858        if (userIdentity == null)
859        {
860            return null;
861        }
862        
863        Set<JCRProjectMember> members = getProjectMembers(project);
864        
865        JCRProjectMember projectMember = members.stream()
866                .filter(member -> MemberType.USER.toString().equals(member.getType()))
867                .filter(member -> userIdentity.equals(member.getUser()))
868                .findFirst()
869                .orElse(null);
870
871        if (projectMember == null)
872        {
873            Set<String> projectPopulations = project.getSites()
874                    .stream()
875                    .map(site -> _populationContextHelper.getUserPopulationsOnContexts(ImmutableList.of("/sites/" + site.getName(), "/sites-fo/" + site.getName()), false, false))
876                    .flatMap(Set::stream)
877                    .collect(Collectors.toSet());
878            
879            Predicate<UserIdentity> isUserInProjectPopulation = user -> projectPopulations.contains(user.getPopulationId()) && userIdentity.equals(user);
880            
881            Predicate<JCRProjectMember> isGroupInProjectPopulations = member -> _getUsersOfGroupMember(member).stream()
882                    .filter(isUserInProjectPopulation)
883                    .findAny()
884                    .isPresent();
885            
886            projectMember = members.stream()
887                    .filter(isGroupInProjectPopulations)
888                    .findFirst()
889                    .orElse(null);
890        }
891        return projectMember;
892    }
893    
894    private Set<UserIdentity> _getUsersOfGroupMember(JCRProjectMember projectMember)
895    {
896        Predicate<JCRProjectMember> isTypeGroup = member -> MemberType.GROUP.toString().equals(member.getType());
897
898        return Optional.ofNullable(projectMember)
899                .filter(isTypeGroup)
900                .map(JCRProjectMember::getGroup)
901                .map(_groupManager::getGroup)
902                .map(Group::getUsers)
903                .orElse(Collections.emptySet());
904    }
905    
906    /**
907     * Set the manager of a project
908     * @param projectName The project name
909     * @param profileId The profile id to affect
910     * @param managers The managers' user identity
911     */
912    public void setProjectManager(String projectName, String profileId, List<UserIdentity> managers)
913    {
914        Project project = _projectManager.getProject(projectName);
915        if (project != null)
916        {
917            // Remove from old managers the rights to edit and delete the project
918            Arrays.stream(project.getManagers())
919                    .filter(((Predicate<UserIdentity>) managers::contains).negate())
920                    .forEach(userIdentity -> _removeManagerRights(project, userIdentity));
921            
922            project.setManagers(managers.toArray(new UserIdentity[managers.size()]));
923            
924            for (UserIdentity userIdentity : managers)
925            {
926                _profileAssignmentStorageExtensionPoint.getAllowedProfilesForUser(project, userIdentity).stream()
927                        .filter(previousProfile -> !profileId.equals(previousProfile) && !RightManager.READER_PROFILE_ID.equals(previousProfile))
928                        .forEach(obsoleteProfile -> _profileAssignmentStorageExtensionPoint.removeAllowedProfileFromUser(userIdentity, obsoleteProfile, project));
929                
930                _profileAssignmentStorageExtensionPoint.allowProfileToUser(userIdentity, RightManager.READER_PROFILE_ID, project);
931                _profileAssignmentStorageExtensionPoint.allowProfileToUser(userIdentity, profileId, project);
932                _notifyAclUpdated(userIdentity, project, Arrays.asList(RightManager.READER_PROFILE_ID, profileId));
933                
934                JCRProjectMember member = getOrCreateProjectMember(project, userIdentity);
935                
936                // Allow the user to access the project's site
937                _allowReadAccessOnProject(member, project);
938                _addProfile(member, profileId, project);
939                
940                for (WorkspaceModule module : _projectManager.getModules(project))
941                {
942                    Set<String> allowedProfiles = Stream.of(RightManager.READER_PROFILE_ID, profileId).collect(Collectors.toSet());
943                    _setProfileOnModule(member, project, module, allowedProfiles);        
944                }
945            }
946            
947            project.saveChanges();
948        }
949    }
950    
951    private void _removeManagerRights(Project project, UserIdentity userIdentity)
952    {
953        _profileAssignmentStorageExtensionPoint.getAllowedProfilesForUser(project, userIdentity).stream()
954                .filter(((Predicate<String>) RightManager.READER_PROFILE_ID::equals).negate()) // keep reader profile for project access, only remove manager specific rights
955                .forEach(profile -> _profileAssignmentStorageExtensionPoint.removeAllowedProfileFromUser(userIdentity, profile, project));
956    }
957    
958    private void _notifyAclUpdated(UserIdentity userIdentity, AmetysObject aclContext, Collection<String> aclProfiles)
959    {
960        Map<String, Object> eventParams = new HashMap<>();
961        eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_CONTEXT, aclContext);
962        eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_CONTEXT_IDENTIFIER, aclContext.getId());
963        eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_PROFILES, aclProfiles);
964        eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_SOLR_CACHE_UNINFLUENTIAL, true);
965        
966        _observationManager.notify(new Event(org.ametys.core.ObservationConstants.EVENT_ACL_UPDATED, userIdentity, eventParams));
967    }
968
969    /**
970     * Retrieve or create a user in a project
971     * @param project The project
972     * @param userIdentity the user
973     * @return The user
974     */
975    public JCRProjectMember getOrCreateProjectMember(Project project, UserIdentity userIdentity)
976    {
977        Predicate<? super AmetysObject> findMemberPredicate = memberNode -> MemberType.USER.toString().equals(((JCRProjectMember) memberNode).getType()) 
978                && userIdentity.equals(((JCRProjectMember) memberNode).getUser());
979        JCRProjectMember projectMember = _getOrCreateProjectMember(project, findMemberPredicate);
980        
981        if (projectMember.needsSave())
982        {
983            projectMember.setUser(userIdentity);
984            projectMember.setType(MemberType.USER);
985        }
986        
987        return projectMember; 
988    }
989
990    /**
991     * Retrieve or create a group as a member in a project
992     * @param project The project
993     * @param groupIdentity the group
994     * @return The user
995     */
996    public JCRProjectMember getOrCreateProjectMember(Project project, GroupIdentity groupIdentity)
997    {
998        Predicate<? super AmetysObject> findMemberPredicate = memberNode -> MemberType.GROUP.toString().equals(((JCRProjectMember) memberNode).getType()) 
999                && groupIdentity.equals(((JCRProjectMember) memberNode).getGroup());
1000        JCRProjectMember projectMember = _getOrCreateProjectMember(project, findMemberPredicate);
1001        
1002        if (projectMember.needsSave())
1003        {
1004            projectMember.setGroup(groupIdentity);
1005            projectMember.setType(MemberType.GROUP);
1006        }
1007        
1008        return projectMember; 
1009    }
1010    
1011    /**
1012     * Retrieve or create a member in a project
1013     * @param project The project
1014     * @param findMemberPredicate The predicate to find the member node
1015     * @return The member node. A new node is created if the member node was not found
1016     */
1017    protected JCRProjectMember _getOrCreateProjectMember(Project project, Predicate<? super AmetysObject> findMemberPredicate)
1018    {
1019        ModifiableTraversableAmetysObject membersNode = _getProjectMembersNode(project);
1020        
1021        Optional<AmetysObject> member = _getProjectMembersNode(project).getChildren()
1022                                                                       .stream()
1023                                                                       .filter(memberNode -> memberNode instanceof JCRProjectMember)
1024                                                                       .filter(findMemberPredicate)
1025                                                                       .findFirst();
1026        if (member.isPresent())
1027        {
1028            return (JCRProjectMember) member.get();
1029        }
1030        
1031        String baseName = "member";
1032        String name = baseName;
1033        int index = 1;
1034        while (membersNode.hasChild(name))
1035        {
1036            index++;
1037            name = baseName + "-" + index;
1038        }
1039        
1040        return membersNode.createChild(name, __PROJECT_MEMBER_NODE_TYPE);
1041    }
1042
1043    
1044    /**
1045     * Remove a user from a project
1046     * @param projectName The project name
1047     * @param identity The identity of the user or group, who must be a member of the project 
1048     * @param type The type of the member, user or group
1049     * @return The error code, if an error occurred
1050     * @throws IllegalAccessException If the user cannot execute this operation
1051     */
1052    @Callable
1053    public Map<String, Object> removeMember(String projectName, String identity, String type) throws IllegalAccessException
1054    {
1055        Map<String, Object> result = new HashMap<>();
1056        
1057        boolean isTypeUser = JCRProjectMember.MemberType.USER.toString().equals(type);
1058        boolean isTypeGroup = JCRProjectMember.MemberType.GROUP.toString().equals(type);
1059        UserIdentity user = Optional.ofNullable(identity)
1060                                    .filter(id -> id != null && isTypeUser)
1061                                    .map(UserIdentity::stringToUserIdentity)
1062                                    .orElse(null);
1063        GroupIdentity group = Optional.ofNullable(identity)
1064                                      .filter(id -> id != null && isTypeGroup)
1065                                      .map(GroupIdentity::stringToGroupIdentity)
1066                                      .orElse(null);
1067        
1068        if ((isTypeGroup && group == null) || (isTypeUser && user == null))
1069        {
1070            result.put("message", isTypeGroup ? "unknow-group" : "unknow-user");
1071            return result;
1072        }
1073        
1074        Project project = _projectManager.getProject(projectName);
1075        if (project == null)
1076        {
1077            result.put("message", "unknow-project");
1078            return result;
1079        }
1080        
1081        if (_isCurrentUser(isTypeUser, user))
1082        {
1083            result.put("message", "current-user");
1084            return result;
1085        }
1086        
1087        // If there is only one manager, do not remove him from the project's members
1088        if (_isOnlyManager(project, isTypeUser, user))
1089        {
1090            result.put("message", "only-manager");
1091            return result;
1092        }
1093        
1094        if (!_projectRightHelper.canRemoveMember(project))
1095        {
1096            throw new IllegalAccessException("User '" + _currentUserProvider.getUser() + "' tried to remove member without convenient right [" + projectName + ", " + identity + "]");
1097        }
1098        
1099        JCRProjectMember projectMember = null;
1100        if (isTypeUser)
1101        {
1102            projectMember = _getProjectMember(project, user);
1103        }
1104        else if (isTypeGroup)
1105        {
1106            projectMember = _getProjectMember(project, group);
1107        }
1108        
1109        if (projectMember == null)
1110        {
1111            result.put("message", "unknow-member");
1112            return result;
1113        }
1114        
1115        _removeManager(project, isTypeUser, user);
1116        _removeMemberProfiles(project, projectMember);
1117
1118        projectMember.remove();
1119        project.saveChanges();
1120        
1121        Map<String, Object> eventParams = new HashMap<>();
1122        eventParams.put(ObservationConstants.ARGS_MEMBER_IDENTITY, identity);
1123        eventParams.put(ObservationConstants.ARGS_MEMBER_IDENTITY_TYPE, type);
1124        eventParams.put(ObservationConstants.ARGS_PROJECT, project);
1125        _observationManager.notify(new Event(ObservationConstants.EVENT_MEMBER_DELETED, _currentUserProvider.getUser(), eventParams));
1126        
1127        return result;
1128    }
1129
1130    private boolean _isCurrentUser(boolean isTypeUser, UserIdentity user)
1131    {
1132        return isTypeUser && _currentUserProvider.getUser().equals(user);
1133    }
1134
1135    private boolean _isOnlyManager(Project project, boolean isTypeUser, UserIdentity user)
1136    {
1137        UserIdentity[] managers = project.getManagers();
1138        return isTypeUser && managers.length == 1 && managers[0].equals(user);
1139    }
1140
1141    private JCRProjectMember _getProjectMember(Project project, GroupIdentity group)
1142    {
1143        JCRProjectMember projectMember = null;
1144        ModifiableTraversableAmetysObject membersNode = _getProjectMembersNode(project);
1145        
1146        for (AmetysObject memberNode : membersNode.getChildren())
1147        {
1148            if (memberNode instanceof JCRProjectMember)
1149            {
1150                JCRProjectMember member = (JCRProjectMember) memberNode;
1151                if (MemberType.GROUP.toString().equals(member.getType()) && group.equals(member.getGroup()))
1152                {
1153                    projectMember = (JCRProjectMember) memberNode;
1154                }
1155
1156            }
1157        }
1158        return projectMember;
1159    }
1160
1161    private JCRProjectMember _getProjectMember(Project project, UserIdentity user)
1162    {
1163        JCRProjectMember projectMember = null;
1164        ModifiableTraversableAmetysObject membersNode = _getProjectMembersNode(project);
1165        
1166        for (AmetysObject memberNode : membersNode.getChildren())
1167        {
1168            if (memberNode instanceof JCRProjectMember)
1169            {
1170                JCRProjectMember member = (JCRProjectMember) memberNode;
1171                if (MemberType.USER.toString().equals(member.getType()) && user.equals(member.getUser()))
1172                {
1173                    projectMember = (JCRProjectMember) memberNode;
1174                }
1175            }
1176        }
1177        return projectMember;
1178    }
1179    
1180    private void _removeManager(Project project, boolean isTypeUser, UserIdentity user)
1181    {
1182        if (isTypeUser)
1183        {
1184            UserIdentity[] oldManagers = project.getManagers();
1185            
1186            // Remove the rights to edit and delete the project from the deleted manager 
1187            Arrays.stream(oldManagers)
1188                  .filter(user::equals)
1189                  .forEach(userIdentity -> _removeManagerRights(project, userIdentity));
1190            
1191            // Remove the user from the project's managers
1192            UserIdentity[] managers = Arrays.stream(oldManagers)
1193                  .filter(manager -> !manager.equals(user))
1194                  .toArray(UserIdentity[]::new);
1195            
1196            project.setManagers(managers);
1197        }
1198    }
1199
1200    private void _removeMemberProfiles(Project project, JCRProjectMember projectMember)
1201    {
1202        // Remove rights on project and project's site
1203        _removeMemberProfiles(projectMember, project);
1204        
1205        for (ModifiablePage dashboardPage : _getProjectDashboardPages(project))
1206        {
1207            _removeMemberProfiles(projectMember, dashboardPage);
1208        }
1209        
1210        for (WorkspaceModule module : _projectManager.getModules(project))
1211        {
1212            ModifiableResourceCollection moduleRootNode = module.getModuleRoot(project, false);
1213            // Remove profiles on module
1214            _removeMemberProfiles(projectMember, moduleRootNode);
1215            
1216            // Remove profiles on module's pages
1217            AmetysObjectIterable<Page> modulePages = module.getModulePages(project, null);
1218            for (Page modulePage : modulePages)
1219            {
1220                _removeMemberProfiles(projectMember, modulePage);
1221            }
1222        }
1223    }
1224    
1225    /**
1226     * Retrieves the users node of the project
1227     * The users node will be created if necessary
1228     * @param project The project
1229     * @return The users node of the project
1230     */
1231    protected ModifiableTraversableAmetysObject _getProjectMembersNode(Project project)
1232    {
1233        if (project == null)
1234        {
1235            throw new AmetysRepositoryException("Error getting the project users node, project is null");
1236        }
1237        
1238        try
1239        {
1240            ModifiableTraversableAmetysObject usersNode;
1241            if (project.hasChild(__PROJECT_MEMBERS_NODE))
1242            {
1243                usersNode = project.getChild(__PROJECT_MEMBERS_NODE);
1244            }
1245            else
1246            {
1247                usersNode = project.createChild(__PROJECT_MEMBERS_NODE, __PROJECT_MEMBERS_NODE_TYPE);
1248            }
1249            
1250            return usersNode;
1251        }
1252        catch (AmetysRepositoryException e)
1253        {
1254            throw new AmetysRepositoryException("Error getting the project users node", e);
1255        }
1256    }
1257    
1258    /**
1259     * Get the JSON representation of a group
1260     * @param group The group
1261     * @return The group
1262     */
1263    protected Map<String, Object> group2Json(Group group)
1264    {
1265        Map<String, Object> infos = new HashMap<>();
1266        infos.put("id", GroupIdentity.groupIdentityToString(group.getIdentity()));
1267        infos.put("groupId", group.getIdentity().getId());
1268        infos.put("label", group.getLabel());
1269        infos.put("sortablename", group.getLabel());
1270        infos.put("groupDirectory", group.getIdentity().getDirectoryId());
1271        return infos;
1272    }
1273
1274    /**
1275     * Count the total of unique users in the project and in the project's group
1276     * @param project The project
1277     * @return The total of members
1278     */
1279    public Long getMembersCount(Project project)
1280    {
1281        Set<JCRProjectMember> projectMembers = getProjectMembers(project);
1282        
1283        Stream<UserIdentity> projectUsers = projectMembers.stream()
1284                .filter(isUserOfType(MemberType.USER))
1285                .map(JCRProjectMember::getUser)
1286                .filter(Objects::nonNull);
1287        
1288        Stream<UserIdentity> projectGroupUsers = projectMembers.stream()
1289                .filter(isUserOfType(MemberType.GROUP))
1290                .map(member -> getGroupUsers(member))
1291                .filter(Objects::nonNull)
1292                .flatMap(Set::stream);
1293        
1294        return Stream.concat(projectUsers, projectGroupUsers)
1295                .distinct()
1296                .count();
1297    }
1298
1299    private Set<UserIdentity> getGroupUsers(JCRProjectMember member)
1300    {
1301        return Optional.ofNullable(member.getGroup())
1302                .map(groupdId -> _groupManager.getGroup(groupdId))
1303                .map(group -> group.getUsers())
1304                .orElse(null);
1305    }
1306
1307    private Predicate<JCRProjectMember> isUserOfType(MemberType memberType)
1308    {
1309        return member -> member.getType().equals(memberType.toString());
1310    }
1311    
1312    /**
1313     * Get the users from a group that are part of the project. They can be filtered with a predicate
1314     * @param group The group
1315     * @param project The project
1316     * @param filteringPredicate The predicate to filter
1317     * @return The list of users
1318     */
1319    public List<User> getGroupUsersFromProject(Group group, Project project, BiPredicate<Project, UserIdentity> filteringPredicate)
1320    {
1321        Set<String> projectPopulations = project.getSites()
1322                .stream()
1323                .map(Site::getName)
1324                .map(siteName -> _populationContextHelper.getUserPopulationsOnContexts(ImmutableList.of("/sites/" + siteName), false, false))
1325                .flatMap(Set::stream)
1326                .collect(Collectors.toSet());
1327        
1328        return group.getUsers().stream()
1329                .filter(user -> projectPopulations.contains(user.getPopulationId()))
1330                .filter(user -> filteringPredicate.test(project, user))
1331                .map(_userManager::getUser)
1332                .filter(Objects::nonNull)
1333                .collect(Collectors.toList());
1334    }
1335    
1336    /**
1337     * This class represents a member of a project. Could be a user or a group
1338     *
1339     */
1340    static class ProjectMember
1341    {
1342        private String _title;
1343        private String _sortableTitle;
1344        private MemberType _type;
1345        private String _role;
1346        private User _user;
1347        private Group _group;
1348        private boolean _isManager;
1349        
1350        /**
1351         * Create a project member as a group
1352         * @param title the member's title (user's full name or group's label)
1353         * @param sortableTitle the sortable title
1354         * @param group the group attached to this member. Cannot be null.
1355         */
1356        public ProjectMember(String title, String sortableTitle, Group group)
1357        {
1358            _title = title;
1359            _sortableTitle = sortableTitle;
1360            _type = MemberType.GROUP;
1361            _role = null;
1362            _isManager = false;
1363            _user = null;
1364            _group = group;
1365        }
1366        
1367        /**
1368         * Create a project member as a group
1369         * @param title the member's title (user's full name or group's label)
1370         * @param sortableTitle the sortable title
1371         * @param role the role
1372         * @param isManager true if the member is a manager of the project
1373         * @param user the user attached to this member. Cannot be null.
1374         */
1375        public ProjectMember(String title, String sortableTitle, User user, String role, boolean isManager)
1376        {
1377            _title = title;
1378            _sortableTitle = sortableTitle;
1379            _type = MemberType.USER;
1380            _role = role;
1381            _isManager = isManager;
1382            _user = user;
1383            _group = null;
1384        }
1385        
1386        String getTitle()
1387        {
1388            return _title;
1389        }
1390        
1391        String getSortableTitle()
1392        {
1393            return _sortableTitle;
1394        }
1395        
1396        MemberType getType()
1397        {
1398            return _type;
1399        }
1400        
1401        String getRole()
1402        {
1403            return _role;
1404        }
1405        
1406        boolean isManager()
1407        {
1408            return _isManager;
1409        }
1410        
1411        User getUser()
1412        {
1413            return _user;
1414        }
1415        
1416        Group getGroup()
1417        {
1418            return _group;
1419        }
1420        
1421        @Override
1422        public boolean equals(Object obj)
1423        {
1424            if (obj == null || !(obj instanceof ProjectMember))
1425            {
1426                return false;
1427            }
1428            
1429            ProjectMember otherMember = (ProjectMember) obj;
1430            
1431            if (getType() != otherMember.getType())
1432            {
1433                return false;
1434            }
1435            
1436            if (getType() == MemberType.USER)
1437            {
1438                return getUser().equals(otherMember.getUser());
1439            }
1440            else
1441            {
1442                return getGroup().equals(otherMember.getGroup());
1443            }
1444        }
1445        
1446        @Override
1447        public int hashCode()
1448        {
1449            return getType() == MemberType.USER ? getUser().getIdentity().hashCode() : getGroup().getIdentity().hashCode();
1450        }
1451    }
1452}