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