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