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