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