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