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 ? _getOrCreateJCRProjectMember(project, user) : _getOrCreateJCRProjectMember(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        boolean userAdded = isTypeUser ? addOrUpdateProjectMember(project, user, newProfiles) : addOrUpdateProjectMember(project, group, newProfiles);
501        
502        result.put("success", userAdded);
503        return result;
504    }
505    
506    /**
507     * Add a user to a project with open inscriptions, using the default values
508     * @param project The project
509     * @param user The user
510     * @return True if the user was successfully added
511     */
512    public boolean addProjectMember(Project project, UserIdentity user)
513    {
514        InscriptionStatus inscriptionStatus = project.getInscriptionStatus();
515        if (!inscriptionStatus.equals(InscriptionStatus.OPEN))
516        {
517            return false;
518        }
519        
520        return addOrUpdateProjectMember(project, user, Map.of());
521    }
522    
523    /**
524     * Add a user to a project, using the provided profile values
525     * @param project The project
526     * @param user The user
527     * @param allowedProfiles the profile values
528     * @return True if the user was successfully added
529     */
530    public boolean addOrUpdateProjectMember(Project project, UserIdentity user, Map<String, String> allowedProfiles)
531    {
532        if (user == null)
533        {
534            return false;
535        }
536        
537        JCRProjectMember projectMember = _getOrCreateJCRProjectMember(project, user);
538        _setMemberProfiles(allowedProfiles, projectMember, project);
539        _saveAndNotifyProjectMemberUpdate(project, projectMember, UserIdentity.userIdentityToString(user), MemberType.USER);
540        return true;
541    }
542    
543    /**
544     * Add a group to a project, using the provided profile values
545     * @param project The project
546     * @param group The group
547     * @param allowedProfiles the profile values
548     * @return True if the user was successfully added
549     */
550    public boolean addOrUpdateProjectMember(Project project, GroupIdentity group, Map<String, String> allowedProfiles)
551    {
552        if (group == null)
553        {
554            return false;
555        }
556        
557        JCRProjectMember projectMember = _getOrCreateJCRProjectMember(project, group);
558        _setMemberProfiles(allowedProfiles, projectMember, project);
559        _saveAndNotifyProjectMemberUpdate(project, projectMember, GroupIdentity.groupIdentityToString(group), MemberType.USER);
560        return true;
561    }
562
563    private void _saveAndNotifyProjectMemberUpdate(Project project, JCRProjectMember projectMember, String userIdentityString, MemberType memberType)
564    {
565        project.saveChanges();
566        
567        _getCache().invalidate(ProjectMemberCacheKey.of(project.getId(), null));
568        
569        // Notify listeners
570        Map<String, Object> eventParams = new HashMap<>();
571        eventParams.put(ObservationConstants.ARGS_MEMBER, projectMember);
572        eventParams.put(ObservationConstants.ARGS_MEMBER_ID, projectMember.getId());
573        eventParams.put(ObservationConstants.ARGS_PROJECT, project);
574        eventParams.put(ObservationConstants.ARGS_PROJECT_ID, project.getId());
575        eventParams.put(ObservationConstants.ARGS_MEMBER_IDENTITY, userIdentityString);
576        eventParams.put(ObservationConstants.ARGS_MEMBER_IDENTITY_TYPE, memberType);
577        _observationManager.notify(new Event(ObservationConstants.EVENT_MEMBER_ADDED, _currentUserProvider.getUser(), eventParams));
578    }
579
580    /**
581     * Set the profiles for a member
582     * @param newProfiles The allowed profile by module
583     * @param projectMember The member
584     * @param project The project
585     */
586    private void _setMemberProfiles(Map<String, String> newProfiles, JCRProjectMember projectMember, Project project)
587    {
588        String defaultProfile = project.getDefaultProfile();
589        Set<String> defaultProfiles;
590        if (StringUtils.isEmpty(defaultProfile))
591        {
592            defaultProfiles = Set.of();
593        }
594        else
595        {
596            defaultProfiles = Set.of(defaultProfile);
597        }
598        
599        for (WorkspaceModule module : _moduleManagerEP.getModules())
600        {
601            Set<String> moduleProfiles;
602            if (newProfiles.containsKey(module.getId()))
603            {
604                String profile = newProfiles.get(module.getId());
605                moduleProfiles = StringUtils.isEmpty(profile) ? Set.of() : Set.of(profile);
606            }
607            else
608            {
609                moduleProfiles = defaultProfiles;
610            }
611            _setProfileOnModule(projectMember, project, module, moduleProfiles);
612        }
613    }
614    
615    private void _setProfileOnModule(JCRProjectMember member, Project project, WorkspaceModule module, Set<String> allowedProfiles)
616    {
617        if (module != null && _projectManager.isModuleActivated(project, module.getId()))
618        {
619            AmetysObject moduleObject = module.getModuleRoot(project, false);
620            _setMemberProfiles(member, allowedProfiles, moduleObject);
621        }
622    }
623    
624    private Set<String> _getAllowedProfile(JCRProjectMember member, AmetysObject object)
625    {
626        if (MemberType.GROUP == member.getType())
627        {
628            Map<GroupIdentity, Map<UserOrGroup, Set<String>>> profilesForGroups = _profileAssignmentStorageExtensionPoint.getProfilesForGroups(object, Set.of(member.getGroup()));
629            return Optional.ofNullable(profilesForGroups.get(member.getGroup())).map(a -> a.get(UserOrGroup.ALLOWED)).orElse(Set.of());
630        }
631        else
632        {
633            Map<UserIdentity, Map<UserOrGroup, Set<String>>> profilesForUsers = _profileAssignmentStorageExtensionPoint.getProfilesForUsers(object, member.getUser());
634            return Optional.ofNullable(profilesForUsers.get(member.getUser())).map(a -> a.get(UserOrGroup.ALLOWED)).orElse(Set.of());
635        }
636    }
637
638    private void _setMemberProfiles(JCRProjectMember member, Set<String> allowedProfiles, AmetysObject object)
639    {
640        Set<String> currentAllowedProfiles = _getAllowedProfile(member, object);
641        
642        Collection<String> profilesToRemove  = CollectionUtils.removeAll(currentAllowedProfiles, allowedProfiles);
643        
644        Collection<String> profilesToAdd  = CollectionUtils.removeAll(allowedProfiles, currentAllowedProfiles);
645        
646        for (String profileId : profilesToRemove)
647        {
648            _removeProfile(member, profileId, object);
649        }
650        
651        for (String profileId : profilesToAdd)
652        {
653            _addProfile(member, profileId, object);
654        }
655        
656        Collection<String> updatedProfiles = CollectionUtils.union(profilesToAdd, profilesToRemove);
657        
658        if (updatedProfiles.size() > 0)
659        {
660            _notifyAclUpdated(_currentUserProvider.getUser(), object, updatedProfiles);
661        }
662    }
663    
664    private void _removeProfile(JCRProjectMember member, String profileId, AmetysObject aclObject)
665    {
666        if (MemberType.GROUP == member.getType())
667        {
668            _profileAssignmentStorageExtensionPoint.removeAllowedProfileFromGroup(member.getGroup(), profileId, aclObject);
669        }
670        else
671        {
672            _profileAssignmentStorageExtensionPoint.removeAllowedProfileFromUser(member.getUser(), profileId, aclObject);
673        }
674    }
675    
676    private void _addProfile(JCRProjectMember member, String profileId, AmetysObject aclObject)
677    {
678        if (MemberType.GROUP == member.getType())
679        {
680            _profileAssignmentStorageExtensionPoint.allowProfileToGroup(member.getGroup(), profileId, aclObject);
681        }
682        else
683        {
684            _profileAssignmentStorageExtensionPoint.allowProfileToUser(member.getUser(), profileId, aclObject);
685        }
686    }
687
688    private void _removeMemberProfiles(JCRProjectMember member, AmetysObject object)
689    {
690        Set<String> currentAllowedProfiles = _getAllowedProfile(member, object);
691        
692        for (String allowedProfile : currentAllowedProfiles)
693        {
694            _removeProfile(member, allowedProfile, object);
695        }
696        
697        if (currentAllowedProfiles.size() > 0)
698        {
699            ((ModifiableAmetysObject) object).saveChanges();
700            
701            Map<String, Object> eventParams = new HashMap<>();
702            eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_CONTEXT, object);
703            eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_CONTEXT_IDENTIFIER, object.getId());
704            eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_PROFILES, currentAllowedProfiles);
705            eventParams.put(org.ametys.cms.ObservationConstants.ARGS_ACL_SOLR_CACHE_UNINFLUENTIAL, true);
706            
707            _observationManager.notify(new Event(org.ametys.core.ObservationConstants.EVENT_ACL_UPDATED, _currentUserProvider.getUser(), eventParams));
708        }
709    }
710    
711    /**
712     * Get the current user information
713     * @return The user
714     */
715    @Callable
716    public Map<String, Object> getCurrentUser()
717    {
718        Map<String, Object> result = new HashMap<>();
719        result.put("user", _userHelper.user2json(_currentUserProvider.getUser()));
720        return result;
721    }
722    
723    /**
724     * Get the members of current project or all the members of all projects in where is no current project
725     * @return The members
726     */
727    @Callable
728    public Map<String, Object> getProjectMembers()
729    {
730        Map<String, Object> result = new HashMap<>();
731        
732        Request request = ContextHelper.getRequest(_context);
733        String projectName = (String) request.getAttribute("projectName");
734        
735        Collection<Project> projects = new ArrayList<>();
736        
737        if (StringUtils.isNotEmpty(projectName))
738        {
739            projects.add(_projectManager.getProject(projectName));
740        }
741        else
742        {
743            _projectManager.getProjects()
744                           .stream()
745                           .forEach(project -> projects.add(project));
746        }
747                                        
748        result.put("users", projects.stream()
749                                    .map(project -> getProjectMembers(project, true))
750                                    .flatMap(Set::stream)
751                                    .map(ProjectMember::getUser)
752                                    .distinct()
753                                    .map(user -> _userHelper.user2json(user, true))
754                                    .collect(Collectors.toList()));
755        
756        return result;
757    }
758    
759    /**
760     * Get the members of a project, sorted by managers, non empty role and name
761     * @param projectName the project's name
762     * @param lang the language to get user content
763     * @return the members of project
764     * @throws IllegalAccessException if an error occurred
765     * @throws AmetysRepositoryException if an error occurred
766     */
767    @Callable
768    public Map<String, Object> getProjectMembers(String projectName, String lang) throws IllegalAccessException, AmetysRepositoryException
769    {
770        return getProjectMembers(projectName, lang, false);
771    }
772    
773    /**
774     * Get the members of a project, sorted by managers, non empty role and name
775     * @param projectName the project's name
776     * @param lang the language to get user content
777     * @param expandGroup true if groups are expanded
778     * @return the members of project
779     * @throws IllegalAccessException if an error occurred
780     * @throws AmetysRepositoryException if an error occurred
781     */
782    @Callable
783    public Map<String, Object> getProjectMembers(String projectName, String lang, boolean expandGroup) throws IllegalAccessException, AmetysRepositoryException
784    {
785        Map<String, Object> result = new HashMap<>();
786        
787        Project project = _projectManager.getProject(projectName);
788        if (project == null)
789        {
790            result.put("message", "unknow-project");
791            result.put("success", false);
792            return result;
793        }
794        if (!_projectRightHelper.hasReadAccess(project))
795        {
796            throw new IllegalAccessException("User '" + _currentUserProvider.getUser() + "' tried to access a privilege feature without reader right in the project " + project.getPath());
797        }
798        
799        List<Map<String, Object>> membersData = new ArrayList<>();
800        
801        Set<ProjectMember> projectMembers = getProjectMembers(project, expandGroup);
802        
803        for (ProjectMember projectMember : projectMembers)
804        {
805            Map<String, Object> memberData = new HashMap<>();
806            
807            memberData.put("type", projectMember.getType().name().toLowerCase());
808            memberData.put("title", projectMember.getTitle());
809            memberData.put("sortabletitle", projectMember.getSortableTitle());
810            memberData.put("manager", projectMember.isManager());
811            
812            String role = projectMember.getRole();
813            if (StringUtils.isNotEmpty(role))
814            {
815                memberData.put("role", role);
816            }
817            
818            User user = projectMember.getUser();
819            if (user != null)
820            {
821                memberData.put("id", UserIdentity.userIdentityToString(user.getIdentity()));
822                memberData.putAll(_userHelper.user2json(user));
823
824                Content userContent = getUserContent(lang, user);
825                
826                if (userContent != null)
827                {
828                    if (userContent.hasValue("function"))
829                    {
830                        memberData.put("function", userContent.getValue("function"));
831                    }
832
833                    if (userContent.hasValue("organisation-accronym"))
834                    {
835                        memberData.put("organisationAcronym", userContent.getValue("organisation-accronym"));
836                    }
837                    String usersDirectorySiteName = _projectManager.getUsersDirectorySiteName();
838                    String[] contentTypes = userContent.getTypes();
839                    for (String contentType : contentTypes)
840                    {
841                        // Try to see if a user page exists for this content type
842                        UserPage userPage = _userDirectoryPageResolver.getUserPage(userContent, usersDirectorySiteName, lang, contentType);
843                        if (userPage != null)
844                        {
845                            memberData.put("link", _uriResolver.getResolverForType("page").resolve(userPage.getId(), false, true, false));
846                        }
847                    }
848                        
849                }
850                else if (getLogger().isDebugEnabled())
851                {
852                    getLogger().debug("User content not found for user : " + user);
853                }
854            }
855            
856            Group group = projectMember.getGroup();
857            if (group != null)
858            {
859                memberData.putAll(group2Json(group));
860            }
861            
862            membersData.add(memberData);
863        }
864        
865        result.put("members", membersData);
866        result.put("success", true);
867        
868        return result;
869    }
870
871    /**
872     * Get user content
873     * @param lang the lang
874     * @param user the user
875     * @return the user content or null if no exist
876     */
877    public Content getUserContent(String lang, User user)
878    {
879        Content userContent = _userDirectoryHelper.getUserContent(user.getIdentity(), lang);
880
881        if (userContent == null)
882        {
883            userContent = _userDirectoryHelper.getUserContent(user.getIdentity(), "en");
884        }
885        
886        if (userContent == null)
887        {
888            Map<String, Language> availableLanguages = _languagesManager.getAvailableLanguages();
889            for (Language availableLanguage : availableLanguages.values())
890            {
891                if (userContent == null)
892                {
893                    userContent = _userDirectoryHelper.getUserContent(user.getIdentity(), availableLanguage.getCode());
894                }
895            }
896        }
897        return userContent;
898    }
899    
900    /**
901     * Get the members of a project, sorted by managers, non empty role and name
902     * @param project the project
903     * @param expandGroup true to expand the user of a group
904     * @return the members of project
905     * @throws AmetysRepositoryException if an error occurred
906     */
907    public Set<ProjectMember> getProjectMembers(Project project, boolean expandGroup) throws AmetysRepositoryException
908    {
909        Cache<ProjectMemberCacheKey, Set<ProjectMember>> cache = _getCache();
910        
911        ProjectMemberCacheKey cacheKey = ProjectMemberCacheKey.of(project.getId(), expandGroup);
912        if (cache.hasKey(cacheKey))
913        {
914            return cache.get(cacheKey);
915        }
916        else
917        {
918            Set<ProjectMember> projectMembers = _getProjectMembers(project, expandGroup);
919            cache.put(cacheKey, projectMembers);
920            return projectMembers;
921        }
922    }
923
924    private Set<ProjectMember> _getProjectMembers(Project project, boolean expandGroup)
925    {
926        Comparator<ProjectMember> managerComparator = Comparator.comparing(m -> m.isManager() ? 0 : 1);
927        Comparator<ProjectMember> roleComparator = Comparator.comparing(m -> StringUtils.isNotBlank(m.getRole()) ? 0 : 1);
928        Comparator<ProjectMember> nameComparator = (m1, m2) -> m1.getSortableTitle().compareToIgnoreCase(m2.getSortableTitle());
929        
930        Set<ProjectMember> members = new TreeSet<>(managerComparator.thenComparing(roleComparator).thenComparing(nameComparator));
931        
932        Map<JCRProjectMember, Object> jcrMembers = _getJCRProjectMembers(project);
933        List<UserIdentity> managers = Arrays.asList(project.getManagers());
934        
935        Set<String> projectPopulations = project.getSites().stream()
936                .map(site -> _populationContextHelper.getUserPopulationsOnContext("/sites/" + site.getName(), false))
937                .flatMap(Set::stream)
938                .collect(Collectors.toSet());
939        
940        Set<String> projectGroupDirectory = project.getSites().stream()
941                .map(site -> _groupDirectoryContextHelper.getGroupDirectoriesOnContext("/sites/" + site.getName()))
942                .flatMap(Set::stream)
943                .collect(Collectors.toSet());
944        
945        for (Entry<JCRProjectMember, Object> entry : jcrMembers.entrySet())
946        {
947            JCRProjectMember jcrMember = entry.getKey();
948            if (MemberType.USER == jcrMember.getType())
949            {
950                User user = (User) entry.getValue();
951                boolean isManager = managers.contains(jcrMember.getUser());
952                
953                ProjectMember projectMember = new ProjectMember(user.getFullName(), user.getSortableName(), user, jcrMember.getRole(), isManager);
954                if (!members.add(projectMember) && projectPopulations.contains(user.getIdentity().getPopulationId())) 
955                {
956                    //if set already contains the user, override it (users always take over users' group)
957                    members.remove(projectMember); // remove the one in  the set
958                    members.add(projectMember); // add the new one
959                }
960            }
961            else if (MemberType.GROUP == jcrMember.getType())
962            {
963                Group group = (Group) entry.getValue();
964                if (projectGroupDirectory.contains(group.getGroupDirectory().getId()))
965                {
966                    if (expandGroup)
967                    {
968                        for (UserIdentity userIdentity : group.getUsers())
969                        {
970                            User user = _userManager.getUser(userIdentity);
971                            if (user != null && projectPopulations.contains(user.getIdentity().getPopulationId()))
972                            {
973                                ProjectMember projectMember = new ProjectMember(user.getFullName(), user.getSortableName(), user, null, false);
974                                members.add(projectMember); // add if does not exist yet
975                            }
976                        }
977                    }
978                    else
979                    {
980                        // Add the member as group
981                        members.add(new ProjectMember(group.getLabel(), group.getLabel(), group));
982                    }
983                }
984            }
985        }
986        return members;
987    }
988    
989    /**
990     * Retrieves the rights for the current user in the project
991     * @param projectName The project Name
992     * @return The project
993     */
994    @Callable
995    public Map<String, Object> getMemberModuleRights(String projectName)
996    {
997        Map<String, Object> results = new HashMap<>();
998        Map<String, Object> rights = new HashMap<>();
999        
1000        Project project = _projectManager.getProject(projectName);
1001        if (project == null)
1002        {
1003            results.put("message", "unknow-project");
1004            results.put("success", false);
1005        }
1006        else
1007        {
1008            rights.put("view", _projectRightHelper.canViewMembers(project));
1009            rights.put("add", _projectRightHelper.canAddMember(project));
1010            rights.put("edit", _projectRightHelper.canEditMember(project));
1011            rights.put("delete", _projectRightHelper.canRemoveMember(project));
1012            results.put("rights", rights);
1013            results.put("success", true);
1014        }
1015        
1016        return results;
1017    }
1018    
1019    /**
1020     * Get the list of users of the project
1021     * @param project The project
1022     * @return The list of users
1023     */
1024    protected  Map<JCRProjectMember, Object> _getJCRProjectMembers(Project project)
1025    {
1026        Map<JCRProjectMember, Object> projectMembers = new HashMap<>();
1027        
1028        if (project != null)
1029        {
1030            ModifiableTraversableAmetysObject membersNode = _getProjectMembersNode(project);
1031            
1032            for (AmetysObject memberNode : membersNode.getChildren())
1033            {
1034                if (memberNode instanceof JCRProjectMember)
1035                {
1036                    JCRProjectMember jCRProjectMember = (JCRProjectMember) memberNode;
1037                    if (jCRProjectMember.getType() == MemberType.USER)
1038                    {
1039                        UserIdentity userIdentity = jCRProjectMember.getUser();
1040                        User user = _userManager.getUser(userIdentity);
1041                        if (user != null)
1042                        {
1043                            projectMembers.put((JCRProjectMember) memberNode, user);
1044                        }
1045                    }
1046                    else
1047                    {
1048                        GroupIdentity groupIdentity = jCRProjectMember.getGroup();
1049                        Group group = _groupManager.getGroup(groupIdentity);
1050                        if (group != null)
1051                        {
1052                            projectMembers.put((JCRProjectMember) memberNode, group);
1053                        }
1054                    }
1055                }
1056            }
1057        }
1058        
1059        return projectMembers;
1060    }
1061    
1062    /**
1063     * Test if an user is a member of a project (directly or by a group)
1064     * @param project The project
1065     * @param userIdentity The user identity
1066     * @return True if this user is a member of this project
1067     */
1068    public boolean isProjectMember(Project project, UserIdentity userIdentity)
1069    {
1070        return getProjectMember(project, userIdentity) != null;
1071    }
1072    
1073    /**
1074     * Retrieve the member of a project corresponding to the user identity
1075     * @param project The project
1076     * @param userIdentity The user identity
1077     * @return The member of this project, which can be of type "user" or "group", or null if the user is not in the project
1078     */
1079    public ProjectMember getProjectMember(Project project, UserIdentity userIdentity)
1080    {
1081        return getProjectMember(project, userIdentity, null);
1082    }
1083    
1084    /**
1085     * Retrieve the member of a project corresponding to the user identity
1086     * @param project The project
1087     * @param userIdentity The user identity
1088     * @param userGroups The user groups. If null the user's groups will be expanded.
1089     * @return The member of this project, which can be of type "user" or "group", or null if the user is not in the project
1090     */
1091    public ProjectMember getProjectMember(Project project, UserIdentity userIdentity, Set<GroupIdentity> userGroups)
1092    {
1093        if (userIdentity == null)
1094        {
1095            return null;
1096        }
1097                
1098        Set<ProjectMember> members = getProjectMembers(project, true);
1099        
1100        ProjectMember projectMember = members.stream()
1101                .filter(member -> MemberType.USER == member.getType())
1102                .filter(member -> userIdentity.equals(member.getUser().getIdentity()))
1103                .findFirst()
1104                .orElse(null);
1105        
1106        if (projectMember != null)
1107        {
1108            return projectMember;
1109        }
1110        
1111        Set<GroupIdentity> groups = userGroups == null ? _groupManager.getUserGroups(userIdentity) : userGroups; // get user's groups
1112
1113        if (!groups.isEmpty())
1114        {
1115            return members.stream()
1116                    .filter(member -> MemberType.GROUP == member.getType())
1117                    .filter(member -> groups.contains(member.getGroup().getIdentity()))
1118                    .findFirst()
1119                    .orElse(null);
1120        }
1121        
1122        return null;
1123    }
1124    
1125    /**
1126     * Set the manager of a project
1127     * @param projectName The project name
1128     * @param profileId The profile id to affect
1129     * @param managers The managers' user identity
1130     */
1131    public void setProjectManager(String projectName, String profileId, List<UserIdentity> managers)
1132    {
1133        Project project = _projectManager.getProject(projectName);
1134        if (project == null)
1135        {
1136            return;
1137        }
1138        
1139        project.setManagers(managers.toArray(new UserIdentity[managers.size()]));
1140        
1141        for (UserIdentity userIdentity : managers)
1142        {
1143            JCRProjectMember member = _getOrCreateJCRProjectMember(project, userIdentity);
1144            
1145            Set<String> allowedProfiles = Set.of(profileId);
1146            for (WorkspaceModule module : _projectManager.getModules(project))
1147            {
1148                _setProfileOnModule(member, project, module, allowedProfiles);        
1149            }
1150        }
1151        
1152        project.saveChanges();
1153        
1154        _getCache().invalidate(ProjectMemberCacheKey.of(project.getId(), null));
1155        
1156        // Clear rights manager cache (if I remove my own rights)
1157        _rightManager.clearCaches();
1158        
1159//        Request request = ContextHelper.getRequest(_context);
1160//        if (request != null)
1161//        {
1162//            request.removeAttribute(RightManager.CACHE_REQUEST_ATTRIBUTE_NAME);
1163//        }
1164    }
1165    
1166    private void _notifyAclUpdated(UserIdentity userIdentity, AmetysObject aclContext, Collection<String> aclProfiles)
1167    {
1168        Map<String, Object> eventParams = new HashMap<>();
1169        eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_CONTEXT, aclContext);
1170        eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_CONTEXT_IDENTIFIER, aclContext.getId());
1171        eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_PROFILES, aclProfiles);
1172        eventParams.put(org.ametys.cms.ObservationConstants.ARGS_ACL_SOLR_CACHE_UNINFLUENTIAL, true);
1173        
1174        _observationManager.notify(new Event(org.ametys.core.ObservationConstants.EVENT_ACL_UPDATED, userIdentity, eventParams));
1175    }
1176
1177    /**
1178     * Retrieve or create a user in a project
1179     * @param project The project
1180     * @param userIdentity the user
1181     * @return The user
1182     */
1183    private JCRProjectMember _getOrCreateJCRProjectMember(Project project, UserIdentity userIdentity)
1184    {
1185        Predicate<? super AmetysObject> findMemberPredicate = memberNode -> MemberType.USER == ((JCRProjectMember) memberNode).getType() 
1186                && userIdentity.equals(((JCRProjectMember) memberNode).getUser());
1187        JCRProjectMember projectMember = _getOrCreateJCRProjectMember(project, findMemberPredicate);
1188        
1189        if (projectMember.needsSave())
1190        {
1191            projectMember.setUser(userIdentity);
1192            projectMember.setType(MemberType.USER);
1193        }
1194        
1195        return projectMember; 
1196    }
1197
1198    /**
1199     * Retrieve or create a group as a member in a project
1200     * @param project The project
1201     * @param groupIdentity the group
1202     * @return The user
1203     */
1204    private JCRProjectMember _getOrCreateJCRProjectMember(Project project, GroupIdentity groupIdentity)
1205    {
1206        Predicate<? super AmetysObject> findMemberPredicate = memberNode -> MemberType.GROUP == ((JCRProjectMember) memberNode).getType() 
1207                && groupIdentity.equals(((JCRProjectMember) memberNode).getGroup());
1208        JCRProjectMember projectMember = _getOrCreateJCRProjectMember(project, findMemberPredicate);
1209        
1210        if (projectMember.needsSave())
1211        {
1212            projectMember.setGroup(groupIdentity);
1213            projectMember.setType(MemberType.GROUP);
1214        }
1215        
1216        return projectMember; 
1217    }
1218    
1219    /**
1220     * Retrieve or create a member in a project
1221     * @param project The project
1222     * @param findMemberPredicate The predicate to find the member node
1223     * @return The member node. A new node is created if the member node was not found
1224     */
1225    protected JCRProjectMember _getOrCreateJCRProjectMember(Project project, Predicate<? super AmetysObject> findMemberPredicate)
1226    {
1227        ModifiableTraversableAmetysObject membersNode = _getProjectMembersNode(project);
1228        
1229        Optional<AmetysObject> member = _getProjectMembersNode(project).getChildren()
1230                                                                       .stream()
1231                                                                       .filter(memberNode -> memberNode instanceof JCRProjectMember)
1232                                                                       .filter(findMemberPredicate)
1233                                                                       .findFirst();
1234        if (member.isPresent())
1235        {
1236            return (JCRProjectMember) member.get();
1237        }
1238        
1239        String baseName = "member";
1240        String name = baseName;
1241        int index = 1;
1242        while (membersNode.hasChild(name))
1243        {
1244            index++;
1245            name = baseName + "-" + index;
1246        }
1247        
1248        JCRProjectMember jcrProjectMember = membersNode.createChild(name, __PROJECT_MEMBER_NODE_TYPE);
1249        
1250        // we invalidate the cache has we had to create a new user
1251        _getCache().invalidate(ProjectMemberCacheKey.of(project.getId(), null));
1252        
1253        return jcrProjectMember;
1254    }
1255
1256    
1257    /**
1258     * Remove a user from a project
1259     * @param projectName The project name
1260     * @param identity The identity of the user or group, who must be a member of the project 
1261     * @param type The type of the member, user or group
1262     * @return The error code, if an error occurred
1263     * @throws IllegalAccessException If the user cannot execute this operation
1264     */
1265    @Callable
1266    public Map<String, Object> removeMember(String projectName, String identity, String type) throws IllegalAccessException
1267    {
1268        Map<String, Object> result = new HashMap<>();
1269        
1270        MemberType memberType = MemberType.valueOf(type.toUpperCase());
1271        boolean isTypeUser = MemberType.USER == memberType;
1272        boolean isTypeGroup = MemberType.GROUP == memberType;
1273        UserIdentity user = Optional.ofNullable(identity)
1274                                    .filter(id -> id != null && isTypeUser)
1275                                    .map(UserIdentity::stringToUserIdentity)
1276                                    .orElse(null);
1277        GroupIdentity group = Optional.ofNullable(identity)
1278                                      .filter(id -> id != null && isTypeGroup)
1279                                      .map(GroupIdentity::stringToGroupIdentity)
1280                                      .orElse(null);
1281        
1282        if (isTypeGroup && group == null
1283            || isTypeUser && user == null)
1284        {
1285            result.put("success", false);
1286            result.put("message", isTypeGroup ? "unknow-group" : "unknow-user");
1287            return result;
1288        }
1289        
1290        Project project = _projectManager.getProject(projectName);
1291        if (project == null)
1292        {
1293            result.put("success", false);
1294            result.put("message", "unknow-project");
1295            return result;
1296        }
1297        
1298        if (_isCurrentUser(isTypeUser, user))
1299        {
1300            result.put("success", false);
1301            result.put("message", "current-user");
1302            return result;
1303        }
1304        
1305        // If there is only one manager, do not remove him from the project's members
1306        if (_isOnlyManager(project, isTypeUser, user))
1307        {
1308            result.put("success", false);
1309            result.put("message", "only-manager");
1310            return result;
1311        }
1312        
1313        if (!_projectRightHelper.canRemoveMember(project))
1314        {
1315            throw new IllegalAccessException("User '" + _currentUserProvider.getUser() + "' tried to remove member without convenient right [" + projectName + ", " + identity + "]");
1316        }
1317        
1318        JCRProjectMember projectMember = null;
1319        if (isTypeUser)
1320        {
1321            projectMember = _getProjectMember(project, user);
1322        }
1323        else if (isTypeGroup)
1324        {
1325            projectMember = _getProjectMember(project, group);
1326        }
1327        
1328        if (projectMember == null)
1329        {
1330            result.put("success", false);
1331            result.put("message", "unknow-member");
1332            return result;
1333        }
1334        
1335        _removeManager(project, isTypeUser, user);
1336        _removeMemberProfiles(project, projectMember);
1337
1338        projectMember.remove();
1339        project.saveChanges();
1340        
1341        _getCache().invalidate(ProjectMemberCacheKey.of(project.getId(), null));
1342        
1343        Map<String, Object> eventParams = new HashMap<>();
1344        eventParams.put(ObservationConstants.ARGS_MEMBER_IDENTITY, identity);
1345        eventParams.put(ObservationConstants.ARGS_MEMBER_IDENTITY_TYPE, memberType);
1346        eventParams.put(ObservationConstants.ARGS_PROJECT, project);
1347        eventParams.put(ObservationConstants.ARGS_PROJECT_ID, project.getId());
1348        _observationManager.notify(new Event(ObservationConstants.EVENT_MEMBER_DELETED, _currentUserProvider.getUser(), eventParams));
1349        
1350        result.put("success", true);
1351        return result;
1352    }
1353
1354    private boolean _isCurrentUser(boolean isTypeUser, UserIdentity user)
1355    {
1356        return isTypeUser && _currentUserProvider.getUser().equals(user);
1357    }
1358
1359    private boolean _isOnlyManager(Project project, boolean isTypeUser, UserIdentity user)
1360    {
1361        UserIdentity[] managers = project.getManagers();
1362        return isTypeUser && managers.length == 1 && managers[0].equals(user);
1363    }
1364
1365    private JCRProjectMember _getProjectMember(Project project, GroupIdentity group)
1366    {
1367        JCRProjectMember projectMember = null;
1368        ModifiableTraversableAmetysObject membersNode = _getProjectMembersNode(project);
1369        
1370        for (AmetysObject memberNode : membersNode.getChildren())
1371        {
1372            if (memberNode instanceof JCRProjectMember)
1373            {
1374                JCRProjectMember member = (JCRProjectMember) memberNode;
1375                if (MemberType.GROUP == member.getType() && group.equals(member.getGroup()))
1376                {
1377                    projectMember = (JCRProjectMember) memberNode;
1378                }
1379
1380            }
1381        }
1382        return projectMember;
1383    }
1384
1385    private JCRProjectMember _getProjectMember(Project project, UserIdentity user)
1386    {
1387        JCRProjectMember projectMember = null;
1388        ModifiableTraversableAmetysObject membersNode = _getProjectMembersNode(project);
1389        
1390        for (AmetysObject memberNode : membersNode.getChildren())
1391        {
1392            if (memberNode instanceof JCRProjectMember)
1393            {
1394                JCRProjectMember member = (JCRProjectMember) memberNode;
1395                if (MemberType.USER == member.getType() && user.equals(member.getUser()))
1396                {
1397                    projectMember = (JCRProjectMember) memberNode;
1398                }
1399            }
1400        }
1401        return projectMember;
1402    }
1403    
1404    private void _removeManager(Project project, boolean isTypeUser, UserIdentity user)
1405    {
1406        if (isTypeUser)
1407        {
1408            UserIdentity[] oldManagers = project.getManagers();
1409            
1410            // Remove the user from the project's managers
1411            UserIdentity[] managers = Arrays.stream(oldManagers)
1412                  .filter(manager -> !manager.equals(user))
1413                  .toArray(UserIdentity[]::new);
1414            
1415            project.setManagers(managers);
1416        }
1417    }
1418
1419    private void _removeMemberProfiles(Project project, JCRProjectMember projectMember)
1420    {
1421        for (WorkspaceModule module : _projectManager.getModules(project))
1422        {
1423            ModifiableResourceCollection moduleRootNode = module.getModuleRoot(project, false);
1424            _removeMemberProfiles(projectMember, moduleRootNode);
1425        }
1426    }
1427    
1428    /**
1429     * Retrieves the users node of the project
1430     * The users node will be created if necessary
1431     * @param project The project
1432     * @return The users node of the project
1433     */
1434    protected ModifiableTraversableAmetysObject _getProjectMembersNode(Project project)
1435    {
1436        if (project == null)
1437        {
1438            throw new AmetysRepositoryException("Error getting the project users node, project is null");
1439        }
1440        
1441        try
1442        {
1443            ModifiableTraversableAmetysObject membersNode;
1444            if (project.hasChild(__PROJECT_MEMBERS_NODE))
1445            {
1446                membersNode = project.getChild(__PROJECT_MEMBERS_NODE);
1447            }
1448            else
1449            {
1450                membersNode = project.createChild(__PROJECT_MEMBERS_NODE, __PROJECT_MEMBERS_NODE_TYPE);
1451            }
1452            
1453            return membersNode;
1454        }
1455        catch (AmetysRepositoryException e)
1456        {
1457            throw new AmetysRepositoryException("Error getting the project users node", e);
1458        }
1459    }
1460    
1461    /**
1462     * Get the JSON representation of a group
1463     * @param group The group
1464     * @return The group
1465     */
1466    protected Map<String, Object> group2Json(Group group)
1467    {
1468        Map<String, Object> infos = new HashMap<>();
1469        infos.put("id", GroupIdentity.groupIdentityToString(group.getIdentity()));
1470        infos.put("groupId", group.getIdentity().getId());
1471        infos.put("label", group.getLabel());
1472        infos.put("sortablename", group.getLabel());
1473        infos.put("groupDirectory", group.getIdentity().getDirectoryId());
1474        return infos;
1475    }
1476
1477    /**
1478     * Count the total of unique users in the project and in the project's group
1479     * @param project The project
1480     * @return The total of members
1481     */
1482    public Long getMembersCount(Project project)
1483    {
1484        Set<ProjectMember> projectMembers = getProjectMembers(project, true);
1485                
1486        return (long) projectMembers.size();
1487    }
1488
1489    /**
1490     * Get the users from a group that are part of the project. They can be filtered with a predicate
1491     * @param group The group
1492     * @param project The project
1493     * @param filteringPredicate The predicate to filter
1494     * @return The list of users
1495     */
1496    public List<User> getGroupUsersFromProject(Group group, Project project, BiPredicate<Project, UserIdentity> filteringPredicate)
1497    {
1498        Set<String> projectPopulations = project.getSites()
1499                .stream()
1500                .map(Site::getName)
1501                .map(siteName -> _populationContextHelper.getUserPopulationsOnContexts(List.of("/sites/" + siteName), false, false))
1502                .flatMap(Set::stream)
1503                .collect(Collectors.toSet());
1504        
1505        return group.getUsers().stream()
1506                .filter(user -> projectPopulations.contains(user.getPopulationId()))
1507                .filter(user -> filteringPredicate.test(project, user))
1508                .map(_userManager::getUser)
1509                .filter(Objects::nonNull)
1510                .collect(Collectors.toList());
1511    }
1512    
1513    private Cache<ProjectMemberCacheKey, Set<ProjectMember>> _getCache()
1514    {
1515        return _abstractCacheManager.get(__PROJECT_MEMBER_CACHE);
1516    }
1517    
1518    /**
1519     * This class represents a member of a project. Could be a user or a group
1520     *
1521     */
1522    public static class ProjectMember
1523    {
1524        private String _title;
1525        private String _sortableTitle;
1526        private MemberType _type;
1527        private String _role;
1528        private User _user;
1529        private Group _group;
1530        private boolean _isManager;
1531        
1532        /**
1533         * Create a project member as a group
1534         * @param title the member's title (user's full name or group's label)
1535         * @param sortableTitle the sortable title
1536         * @param group the group attached to this member. Cannot be null.
1537         */
1538        public ProjectMember(String title, String sortableTitle, Group group)
1539        {
1540            _title = title;
1541            _sortableTitle = sortableTitle;
1542            _type = MemberType.GROUP;
1543            _role = null;
1544            _isManager = false;
1545            _user = null;
1546            _group = group;
1547        }
1548        
1549        /**
1550         * Create a project member as a group
1551         * @param title the member's title (user's full name or group's label)
1552         * @param sortableTitle the sortable title
1553         * @param role the role
1554         * @param isManager true if the member is a manager of the project
1555         * @param user the user attached to this member. Cannot be null.
1556         */
1557        public ProjectMember(String title, String sortableTitle, User user, String role, boolean isManager)
1558        {
1559            _title = title;
1560            _sortableTitle = sortableTitle;
1561            _type = MemberType.USER;
1562            _role = role;
1563            _isManager = isManager;
1564            _user = user;
1565            _group = null;
1566        }
1567        
1568        /**
1569         * Get the title of the member.
1570         * @return The title of the member
1571         */
1572        public String getTitle()
1573        {
1574            return _title;
1575        }
1576        
1577        /**
1578         * Get the sortable title of the member.
1579         * @return The sortable title of the member
1580         */
1581        public String getSortableTitle()
1582        {
1583            return _sortableTitle;
1584        }
1585
1586        /**
1587         * Get the type of the member. It can be a user or a group
1588         * @return The type of the member
1589         */
1590        public MemberType getType()
1591        {
1592            return _type;
1593        }
1594
1595        /**
1596         * Get the role of the member.
1597         * @return The role of the member
1598         */
1599        public String getRole()
1600        {
1601            return _role;
1602        }
1603
1604        /**
1605         * Test if the member is a manager of the project
1606         * @return True if this user is a manager of the project
1607         */
1608        public boolean isManager()
1609        {
1610            return _isManager;
1611        }
1612        
1613        /**
1614         * Get the user of the member.
1615         * @return The user of the member
1616         */
1617        public User getUser()
1618        {
1619            return _user;
1620        }
1621        
1622        /**
1623         * Get the group of the member.
1624         * @return The group of the member
1625         */
1626        public Group getGroup()
1627        {
1628            return _group;
1629        }
1630        
1631        @Override
1632        public boolean equals(Object obj)
1633        {
1634            if (obj == null || !(obj instanceof ProjectMember))
1635            {
1636                return false;
1637            }
1638            
1639            ProjectMember otherMember = (ProjectMember) obj;
1640            
1641            if (getType() != otherMember.getType())
1642            {
1643                return false;
1644            }
1645            
1646            if (getType() == MemberType.USER)
1647            {
1648                return getUser().equals(otherMember.getUser());
1649            }
1650            else
1651            {
1652                return getGroup().equals(otherMember.getGroup());
1653            }
1654        }
1655        
1656        @Override
1657        public int hashCode()
1658        {
1659            return getType() == MemberType.USER ? getUser().getIdentity().hashCode() : getGroup().getIdentity().hashCode();
1660        }
1661    }
1662    
1663    private static final class ProjectMemberCacheKey extends AbstractCacheKey
1664    {
1665        public ProjectMemberCacheKey(String projectId, Boolean extendGroup)
1666        {
1667            super(projectId, extendGroup);
1668        }
1669        
1670        public static ProjectMemberCacheKey of(String projectId, Boolean withExpandedGroup)
1671        {
1672            return new ProjectMemberCacheKey(projectId, withExpandedGroup);
1673        }
1674    }
1675}