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.activities.projects;
017
018import java.util.HashMap;
019import java.util.List;
020import java.util.Map;
021import java.util.Objects;
022import java.util.stream.Stream;
023
024import javax.jcr.RepositoryException;
025
026import org.apache.avalon.framework.service.ServiceException;
027import org.apache.avalon.framework.service.ServiceManager;
028
029import org.ametys.core.group.Group;
030import org.ametys.core.group.GroupIdentity;
031import org.ametys.core.group.GroupManager;
032import org.ametys.core.observation.Event;
033import org.ametys.core.user.User;
034import org.ametys.core.user.UserIdentity;
035import org.ametys.core.user.UserManager;
036import org.ametys.plugins.repository.AmetysObject;
037import org.ametys.plugins.repository.activities.Activity;
038import org.ametys.plugins.repository.activities.ActivityType;
039import org.ametys.plugins.repository.query.expression.Expression;
040import org.ametys.plugins.workspaces.ObservationConstants;
041import org.ametys.plugins.workspaces.activities.AbstractWorkspacesActivityType;
042import org.ametys.plugins.workspaces.members.JCRProjectMember;
043import org.ametys.plugins.workspaces.members.JCRProjectMember.MemberType;
044import org.ametys.plugins.workspaces.members.ProjectMemberManager;
045import org.ametys.plugins.workspaces.members.ProjectMemberManager.ProjectMember;
046import org.ametys.plugins.workspaces.project.notification.preferences.NotificationPreferencesHelper.Frequency;
047import org.ametys.plugins.workspaces.project.objects.Project;
048import org.ametys.runtime.config.Config;
049
050/**
051 * {@link ActivityType} implementation for the addition of a member
052 */
053public class MemberAddedActivityType extends AbstractProjectsActivityType
054{
055    /** data name for the member type */
056    public static final String MEMBER_TYPE = "memberType";
057    /** data name for the member identity */
058    public static final String MEMBER = "member";
059    
060    private UserManager _userManager;
061    private GroupManager _groupManager;
062    private ProjectMemberManager _projectMemberManager;
063
064    @Override
065    public void service(ServiceManager serviceManager) throws ServiceException
066    {
067        super.service(serviceManager);
068        _projectMemberManager = (ProjectMemberManager) serviceManager.lookup(ProjectMemberManager.ROLE);
069        _userManager = (UserManager) serviceManager.lookup(UserManager.ROLE);
070        _groupManager = (GroupManager) serviceManager.lookup(GroupManager.ROLE);
071    }
072    
073    @Override
074    public boolean support(Event event)
075    {
076        return Config.getInstance().getValue("workspaces.member.added.send.notification", true, false) // Check feature enabled
077                && _isNewUser(event);
078    }
079    
080    private boolean _isNewUser(Event event)
081    {
082        Map<String, Object> args = event.getArguments();
083        Project project = getProjectFromEvent(event);
084        JCRProjectMember member = (JCRProjectMember) args.get(ObservationConstants.ARGS_MEMBER);
085        if (MemberType.USER.equals(member.getType()))
086        {
087            if (_userIsPartOfProjectGroups(member.getUser(), project))
088            {
089                // if the user is already present via a group, we ignore the event
090                return false;
091            }
092        }
093        return true;
094    }
095    
096    private boolean _userIsPartOfProjectGroups(UserIdentity identity, Project project)
097    {
098        boolean alreadyPresent = _projectMemberManager.getProjectMembers(project, false)
099                .stream()
100                // retrieve the group
101                .map(ProjectMember::getGroup)
102                .filter(Objects::nonNull)
103                // retrieve group users
104                .map(Group::getUsers)
105                // check if a group contains our user
106                .anyMatch(users -> users.contains(identity));
107        return alreadyPresent;
108    }
109    
110    @Override
111    public void setAdditionalActivityData(Activity activity, Map<String, Object> parameters) throws RepositoryException
112    {
113        super.setAdditionalActivityData(activity, parameters);
114        
115        JCRProjectMember member = (JCRProjectMember) parameters.get(org.ametys.plugins.workspaces.ObservationConstants.ARGS_MEMBER);
116        member = _resolver.resolveById(member.getId());
117        activity.setValue(MEMBER, MemberType.GROUP == member.getType() 
118                ? GroupIdentity.groupIdentityToString(member.getGroup()) 
119                : UserIdentity.userIdentityToString(member.getUser()));
120        activity.setValue(MEMBER_TYPE, member.getType().name().toLowerCase());
121    }
122    
123    @Override
124    public Map<String, Object> additionnalDataToJSONForClient(Activity activity)
125    {
126        // Override the MEMBER element to provide the fullName or Label instead of the identity
127        Map<String, Object> json = super.additionnalDataToJSONForClient(activity);
128        
129        String memberIdentity = activity.getValue(MEMBER);
130        String type = activity.hasValue(MEMBER_TYPE) ? activity.getValue(MEMBER_TYPE) : MemberType.USER.name();
131        if (MemberType.GROUP == MemberType.valueOf(type.toUpperCase()))
132        {
133            GroupIdentity groupIdentity = GroupIdentity.stringToGroupIdentity(memberIdentity);
134            Group group = _groupManager.getGroup(groupIdentity);
135            json.put(MEMBER, group != null ? group.getLabel() : memberIdentity);
136        }
137        else if (MemberType.USER == MemberType.valueOf(type.toUpperCase()))
138        {
139            UserIdentity userIdentity = UserIdentity.stringToUserIdentity(memberIdentity);
140            User user = _userManager.getUser(userIdentity);
141            
142            json.put(MEMBER, user != null ? user.getFullName() : (userIdentity != null ? userIdentity.getLogin() : memberIdentity));
143        }
144        json.put("identity", memberIdentity);
145        
146        return json;
147    }
148    
149    @Override
150    public Expression getFilterPatternExpression(String pattern)
151    {
152        return null;
153    }
154    
155    @Override
156    public Map<String, Object> mergeActivities(List<Activity> activities)
157    {
158        Map<String, Object> mergedActivities = super.mergeActivities(activities);
159        
160        Map<String, Map<String, Object>> members = new HashMap<>();
161        
162        for (Activity activity : activities)
163        {
164            if (activity.hasValue(MEMBER))
165            {
166                Map<String, Object> json = activity.toJSONForClient();
167                String identity = (String) json.get("identity");
168                if (!members.containsKey(identity))
169                {
170                    Map<String, Object> m = new HashMap<>();
171                    m.put("name", json.get(MEMBER));
172                    m.put("type", json.get(MEMBER_TYPE));
173                    m.put("identity", identity);
174                    members.put(identity, m);
175                }
176            }
177        }
178        
179        mergedActivities.put("members", members.values());
180        return mergedActivities;
181    }
182    
183    @Override
184    public Project getProjectFromEvent(Event event)
185    {
186        Map<String, Object> args = event.getArguments();
187        
188        return (Project) args.get(ObservationConstants.ARGS_PROJECT);
189    }
190    
191    @Override
192    public AmetysObject getTargetAmetysObject(Activity activity)
193    {
194        // we don't need this method as we override the AbstractWorkspacesActivityObserver::getUsersEmailToNotify method
195        return null;
196    }
197
198    /**
199     * Get the user to notify Retrieve the previous user in project by removing
200     * the new user or group from the members
201     */
202    @Override
203    public List<String> getUsersEmailToNotify(Activity activity)
204    {
205        Stream<User> previousUser = null;
206        Project project = _projectManager.getProject(activity.getValue(PROJECT_NAME));
207
208        // if the new member is a user, we just need to inform all other user
209        // we have already check that it is in fact a new member
210        MemberType memberType = MemberType.valueOf(activity.<String> getValue(MemberAddedActivityType.MEMBER_TYPE).toUpperCase());
211        if (MemberType.USER.equals(memberType))
212        {
213            UserIdentity userIdentity = UserIdentity.stringToUserIdentity(activity.getValue(MemberAddedActivityType.MEMBER));
214            if (userIdentity != null)
215            {
216                User newUser = _userManager.getUser(userIdentity);
217                if (newUser != null)
218                {
219                    previousUser = _projectMemberManager.getProjectMembers(project, true).stream().map(ProjectMember::getUser).filter(u -> !u.equals(newUser));
220                }
221            }
222        }
223        else
224        {
225            GroupIdentity groupIdentity = GroupIdentity.stringToGroupIdentity(activity.getValue(MemberAddedActivityType.MEMBER));
226            if (groupIdentity != null)
227            {
228                Group group = _groupManager.getGroup(groupIdentity);
229                if (group != null)
230                {
231                    previousUser = _projectMemberManager.getProjectMembers(project, false).stream()
232                            // we remove the new group from the project members.
233                            .filter(member -> MemberType.USER.equals(member.getType()) || group.equals(member.getGroup()))
234                            // now we can expand all the group. If a user from
235                            // the new group was already present he shall
236                            // receive a notification as an activity was created
237                            .flatMap(member -> {
238                                if (MemberType.USER.equals(member.getType()))
239                                {
240                                    return Stream.<User> of(member.getUser());
241                                }
242                                else
243                                {
244                                    return member.getGroup().getUsers().stream().map(_userManager::getUser);
245                                }
246                            });
247                }
248            }
249        }
250        if (previousUser != null)
251        {
252            return previousUser.filter(user -> _notificationPreferenceHelper.askedToBeNotified(user.getIdentity(), activity.getValue(AbstractWorkspacesActivityType.PROJECT_NAME),
253                    Frequency.EACH)).map(User::getEmail).toList();
254        }
255        else
256        {
257            // Something bad happened…
258            return List.of();
259        }
260    }
261    
262    
263    @Override
264    public String getMailBodyURI(Activity activity)
265    {
266        return "cocoon://_plugins/workspaces/notification-mail-member";
267    }
268}