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