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.Date; 020import java.util.GregorianCalendar; 021import java.util.HashMap; 022import java.util.List; 023import java.util.Map; 024import java.util.Map.Entry; 025import java.util.Set; 026 027import javax.jcr.Node; 028import javax.jcr.NodeIterator; 029import javax.jcr.PathNotFoundException; 030import javax.jcr.Repository; 031import javax.jcr.RepositoryException; 032import javax.jcr.Session; 033import javax.jcr.Value; 034import javax.jcr.ValueFormatException; 035import javax.jcr.query.Query; 036 037import org.apache.avalon.framework.service.ServiceException; 038import org.apache.avalon.framework.service.ServiceManager; 039import org.apache.commons.lang3.StringUtils; 040 041import org.ametys.core.user.UserIdentity; 042import org.ametys.plugins.explorer.resources.ModifiableResourceCollection; 043import org.ametys.plugins.explorer.resources.jcr.JCRResourcesCollection; 044import org.ametys.plugins.repository.AmetysRepositoryException; 045import org.ametys.plugins.repository.RepositoryConstants; 046import org.ametys.plugins.repository.data.holder.ModifiableModelAwareDataHolder; 047import org.ametys.plugins.repository.jcr.NodeTypeHelper; 048import org.ametys.plugins.repository.provider.AbstractRepository; 049import org.ametys.plugins.repository.query.SortCriteria; 050import org.ametys.plugins.repository.query.expression.Expression; 051import org.ametys.plugins.repository.query.expression.Expression.Operator; 052import org.ametys.plugins.repository.query.expression.StringExpression; 053import org.ametys.plugins.workspaces.AbstractWorkspaceModule; 054import org.ametys.plugins.workspaces.ObservationConstants; 055import org.ametys.plugins.workspaces.members.JCRProjectMember.MemberType; 056import org.ametys.plugins.workspaces.members.ProjectMemberManager.ProjectMember; 057import org.ametys.plugins.workspaces.project.modules.WorkspaceModule; 058import org.ametys.plugins.workspaces.project.modules.WorkspaceModuleExtensionPoint; 059import org.ametys.plugins.workspaces.project.objects.Project; 060import org.ametys.plugins.workspaces.util.StatisticColumn; 061import org.ametys.plugins.workspaces.util.StatisticsColumnType; 062import org.ametys.runtime.i18n.I18nizableText; 063import org.ametys.web.repository.page.ModifiablePage; 064import org.ametys.web.repository.page.ModifiableZone; 065import org.ametys.web.repository.page.ModifiableZoneItem; 066import org.ametys.web.repository.page.ZoneItem.ZoneType; 067 068import com.google.common.collect.ImmutableSet; 069 070/** 071 * Helper component for managing members 072 */ 073public class MembersWorkspaceModule extends AbstractWorkspaceModule 074{ 075 /** The id of members module */ 076 public static final String MEMBERS_MODULE_ID = MembersWorkspaceModule.class.getName(); 077 078 /** Id of service of members */ 079 public static final String MEMBERS_SERVICE_ID = "org.ametys.plugins.workspaces.module.Members"; 080 081 /** Workspaces members node name */ 082 private static final String __WORKSPACES_MEMBERS_NODE_NAME = "members"; 083 084 private static final String __INVITATIONS_NODE_NAME = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":invitations"; 085 086 private static final String __INVITATION_NODE_NAME = RepositoryConstants.NAMESPACE_PREFIX + ":invitation"; 087 088 private static final String __NODETYPE_INVITATIONS = RepositoryConstants.NAMESPACE_PREFIX + ":invitations"; 089 090 private static final String __NODETYPE_INVITATION = RepositoryConstants.NAMESPACE_PREFIX + ":invitation"; 091 092 private static final String __MEMBER_NUMBER_HEADER_ID = __WORKSPACES_MEMBERS_NODE_NAME + "$member_number"; 093 094 /** Constants for invitation's date property */ 095 private static final String __INVITATION_DATE = RepositoryConstants.NAMESPACE_PREFIX + ":date"; 096 /** Constants for invitation's mail property */ 097 private static final String __INVITATION_MAIL = RepositoryConstants.NAMESPACE_PREFIX + ":mail"; 098 /** Constants for invitation's author property */ 099 private static final String __INVITATION_AUTHOR = RepositoryConstants.NAMESPACE_PREFIX + ":author"; 100 /** Constants for invitation's acl node */ 101 private static final String __INVITATION_ACL = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":temp-acl"; 102 103 private WorkspaceModuleExtensionPoint _moduleEP; 104 105 private Repository _repository; 106 107 private ProjectMemberManager _projectMemberManager; 108 109 @Override 110 public void service(ServiceManager smanager) throws ServiceException 111 { 112 super.service(smanager); 113 _moduleEP = (WorkspaceModuleExtensionPoint) smanager.lookup(WorkspaceModuleExtensionPoint.ROLE); 114 _repository = (Repository) smanager.lookup(AbstractRepository.ROLE); 115 _projectMemberManager = (ProjectMemberManager) smanager.lookup(ProjectMemberManager.ROLE); 116 } 117 118 @Override 119 public String getId() 120 { 121 return MEMBERS_MODULE_ID; 122 } 123 124 public int getOrder() 125 { 126 return ORDER_MEMBERS; 127 } 128 129 public String getModuleName() 130 { 131 return __WORKSPACES_MEMBERS_NODE_NAME; 132 } 133 134 @Override 135 protected String getModulePageName() 136 { 137 return "members"; 138 } 139 140 public I18nizableText getModuleTitle() 141 { 142 return new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_SERVICE_MODULE_MEMBERS_LABEL"); 143 } 144 public I18nizableText getModuleDescription() 145 { 146 return new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_SERVICE_MODULE_MEMBERS_DESCRIPTION"); 147 } 148 @Override 149 protected I18nizableText getModulePageTitle() 150 { 151 return new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_WORKSPACE_PAGE_MEMBERS_TITLE"); 152 } 153 154 @Override 155 protected void initializeModulePage(ModifiablePage memberPage) 156 { 157 ModifiableZone defaultZone = memberPage.createZone("default"); 158 159 ModifiableZoneItem defaultZoneItem = defaultZone.addZoneItem(); 160 defaultZoneItem.setType(ZoneType.SERVICE); 161 defaultZoneItem.setServiceId(MEMBERS_SERVICE_ID); 162 163 ModifiableModelAwareDataHolder params = defaultZoneItem.getServiceParameters(); 164 165 params.setValue("header", _i18nUtils.translate(getModulePageTitle(), memberPage.getSitemapName())); 166 // params.setValue("expandGroup", false); FIXME new service 167 // params.setValue("nbMembers", -1); FIXME new service 168 params.setValue("xslt", _getDefaultXslt(MEMBERS_SERVICE_ID)); 169 } 170 171 @Override 172 public Set<String> getAllowedEventTypes() 173 { 174 return ImmutableSet.of(ObservationConstants.EVENT_MEMBER_ADDED, 175 ObservationConstants.EVENT_MEMBER_JOINED, 176 ObservationConstants.EVENT_MEMBER_UPDATED); 177 } 178 179 // ------------------------------------------------- 180 // INVITATIONS 181 // ------------------------------------------------- 182 183 /** 184 * Get the node holding the invitations 185 * @param project The project 186 * @param create true to create the node if it does not exist 187 * @return the invitations' root node 188 * @throws RepositoryException if an error occurred 189 */ 190 public Node getInvitationsRootNode(Project project, boolean create) throws RepositoryException 191 { 192 ModifiableResourceCollection moduleRoot = getModuleRoot(project, create); 193 194 Node moduleNode = ((JCRResourcesCollection) moduleRoot).getNode(); 195 Node node = null; 196 197 if (moduleNode.hasNode(__INVITATIONS_NODE_NAME)) 198 { 199 node = moduleNode.getNode(__INVITATIONS_NODE_NAME); 200 } 201 else if (create) 202 { 203 node = moduleNode.addNode(__INVITATIONS_NODE_NAME, __NODETYPE_INVITATIONS); 204 moduleNode.getSession().save(); 205 } 206 207 return node; 208 } 209 210 /** 211 * Get the invitations for a given mail 212 * @param email the email 213 * @return the invitations 214 */ 215 public List<Invitation> getInvitations(String email) 216 { 217 Session session = null; 218 try 219 { 220 session = _repository.login(); 221 222 Expression expr = new StringExpression("mail", Operator.EQ, email); 223 String xPathQuery = getInvitationXPathQuery(null, expr, null); 224 225 @SuppressWarnings("deprecation") 226 Query query = session.getWorkspace().getQueryManager().createQuery(xPathQuery, Query.XPATH); 227 NodeIterator nodes = query.execute().getNodes(); 228 229 List<Invitation> invitations = new ArrayList<>(); 230 231 while (nodes.hasNext()) 232 { 233 Node node = (Node) nodes.next(); 234 invitations.add(_getInvitation(node)); 235 } 236 237 return invitations; 238 } 239 catch (RepositoryException ex) 240 { 241 if (session != null) 242 { 243 session.logout(); 244 } 245 246 throw new AmetysRepositoryException("An error occurred executing the JCR query to get invitations for email " + email, ex); 247 } 248 } 249 250 /** 251 * Returns the invitation sorted by ascending date 252 * @param project The project 253 * @return The invitations node 254 * @throws RepositoryException if an error occurred 255 */ 256 public List<Invitation> getInvitations(Project project) throws RepositoryException 257 { 258 List<Invitation> invitations = new ArrayList<>(); 259 260 SortCriteria sortCriteria = new SortCriteria(); 261 sortCriteria.addJCRPropertyCriterion(__INVITATION_DATE, false, false); 262 263 String xPathQuery = getInvitationXPathQuery(project, null, sortCriteria); 264 265 @SuppressWarnings("deprecation") 266 Query query = project.getNode().getSession().getWorkspace().getQueryManager().createQuery(xPathQuery, Query.XPATH); 267 NodeIterator nodes = query.execute().getNodes(); 268 269 while (nodes.hasNext()) 270 { 271 Node node = (Node) nodes.next(); 272 invitations.add(_getInvitation(node)); 273 } 274 275 return invitations; 276 } 277 278 private Invitation _getInvitation(Node node) throws ValueFormatException, PathNotFoundException, RepositoryException 279 { 280 String email = node.getProperty(__INVITATION_MAIL).getString(); 281 Date date = node.getProperty(__INVITATION_DATE).getDate().getTime(); 282 Node authorNode = node.getNode(__INVITATION_AUTHOR); 283 UserIdentity author = new UserIdentity(authorNode.getProperty(RepositoryConstants.NAMESPACE_PREFIX + ":login").getString(), authorNode.getProperty(RepositoryConstants.NAMESPACE_PREFIX + ":population").getString()); 284 285 Map<String, String> allowedProfileByModules = new HashMap<>(); 286 287 Node aclNode = node.getNode(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":temp-acl"); 288 NodeIterator children = aclNode.getNodes(); 289 while (children.hasNext()) 290 { 291 Node child = (Node) children.next(); 292 if (child.hasProperty(RepositoryConstants.NAMESPACE_PREFIX + ":allowed-profiles")) 293 { 294 Value[] values = child.getProperty(RepositoryConstants.NAMESPACE_PREFIX + ":allowed-profiles").getValues(); 295 if (values.length > 0) 296 { 297 String moduleName = child.getName(); 298 WorkspaceModule module = _moduleEP.getModuleByName(moduleName); 299 if (module != null) 300 { 301 allowedProfileByModules.put(module.getId(), values[0].getString()); 302 } 303 } 304 } 305 } 306 307 return new Invitation(email, date, author, allowedProfileByModules, _getProjectName(node)); 308 } 309 310 private String _getProjectName(Node node) 311 { 312 try 313 { 314 Node parentNode = node.getParent(); 315 316 while (parentNode != null && !NodeTypeHelper.isNodeType(parentNode, "ametys:project")) 317 { 318 parentNode = parentNode.getParent(); 319 } 320 321 return parentNode != null ? parentNode.getName() : null; 322 } 323 catch (RepositoryException e) 324 { 325 getLogger().error("Unable to get parent project", e); 326 return null; 327 } 328 } 329 330 /** 331 * Creates the XPath query corresponding to specified {@link Expression}. 332 * @param project The project. Can be null to browser all projects 333 * @param invitExpression the query predicates. 334 * @param sortCriteria the sort criteria. 335 * @return the created XPath query. 336 * @throws RepositoryException if an error occurred 337 */ 338 public String getInvitationXPathQuery(Project project, Expression invitExpression, SortCriteria sortCriteria) throws RepositoryException 339 { 340 String predicats = null; 341 342 if (invitExpression != null) 343 { 344 predicats = StringUtils.trimToNull(invitExpression.build()); 345 } 346 347 StringBuilder xpathQuery = new StringBuilder(); 348 349 if (project != null) 350 { 351 xpathQuery.append("/jcr:root") 352 .append(getInvitationsRootNode(project, true).getPath()); 353 } 354 355 xpathQuery.append("//element(*, " + __NODETYPE_INVITATION + ")"); 356 357 if (predicats != null) 358 { 359 xpathQuery.append("[" + predicats + "]"); 360 } 361 362 if (sortCriteria != null) 363 { 364 xpathQuery.append(" " + sortCriteria.build()); 365 } 366 367 return xpathQuery.toString(); 368 } 369 370 /** 371 * Add an invitation 372 * @param project The project 373 * @param mail The mail 374 * @param invitDate The invitation's date 375 * @param author The invitation's author 376 * @param allowedProfileByModules The allowed profiles by modules 377 * @return The created invitation node 378 * @throws RepositoryException if an error occurred 379 */ 380 public Invitation addInvitation (Project project, Date invitDate, String mail, UserIdentity author, Map<String, String> allowedProfileByModules) throws RepositoryException 381 { 382 Node contextNode = getInvitationsRootNode(project, true); 383 384 Node invitNode = contextNode.addNode(__INVITATION_NODE_NAME, __NODETYPE_INVITATION); 385 386 // Date 387 GregorianCalendar gc = new GregorianCalendar(); 388 gc.setTime(invitDate); 389 390 invitNode.setProperty(__INVITATION_DATE, gc); 391 392 // Mail 393 invitNode.setProperty(__INVITATION_MAIL, mail); 394 395 // Author 396 Node authorNode = invitNode.addNode(__INVITATION_AUTHOR); 397 authorNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ":login", author.getLogin()); 398 authorNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ":population", author.getPopulationId()); 399 400 Node aclNode = invitNode.addNode(__INVITATION_ACL); 401 402 for (Entry<String, String> allowedProfile : allowedProfileByModules.entrySet()) 403 { 404 String moduleId = allowedProfile.getKey(); 405 String profileId = allowedProfile.getValue(); 406 407 WorkspaceModule module = _moduleEP.getExtension(moduleId); 408 if (module != null) 409 { 410 Node moduleNode = aclNode.addNode(module.getModuleName()); 411 moduleNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ":allowed-profiles", new String[] {profileId}); 412 } 413 } 414 415 return new Invitation(mail, invitDate, author, allowedProfileByModules, project.getName()); 416 } 417 418 /** 419 * Determines if a invitation already exists for this email 420 * @param project the project 421 * @param mail the mail to test 422 * @return true if a invitation exists 423 * @throws RepositoryException if an error occured 424 */ 425 public boolean invitationExists(Project project, String mail) throws RepositoryException 426 { 427 Node contextNode = getInvitationsRootNode(project, true); 428 429 NodeIterator nodes = contextNode.getNodes(__INVITATION_NODE_NAME); 430 while (nodes.hasNext()) 431 { 432 Node node = (Node) nodes.next(); 433 if (node.hasProperty(__INVITATION_MAIL) && node.getProperty(__INVITATION_MAIL).getString().equals(mail)) 434 { 435 return true; 436 } 437 } 438 439 return false; 440 } 441 442 /** 443 * Remove a invitation 444 * @param project the project 445 * @param mail the mail to remove 446 * @return true if a invitation has been removed 447 * @throws RepositoryException if an error occured 448 */ 449 public boolean removeInvitation(Project project, String mail) throws RepositoryException 450 { 451 Node contextNode = getInvitationsRootNode(project, true); 452 453 NodeIterator nodes = contextNode.getNodes(__INVITATION_NODE_NAME); 454 while (nodes.hasNext()) 455 { 456 Node node = (Node) nodes.next(); 457 if (node.hasProperty(__INVITATION_MAIL) && node.getProperty(__INVITATION_MAIL).getString().equals(mail)) 458 { 459 node.remove(); 460 return true; 461 } 462 } 463 464 return false; 465 } 466 467 /** 468 * Bean representing a invitation by email 469 * 470 */ 471 public class Invitation 472 { 473 private Date _date; 474 private UserIdentity _author; 475 private String _email; 476 private Map<String, String> _allowedProfileByModules; 477 private String _projectName; 478 479 /** 480 * Constructor 481 * @param email The email 482 * @param date The date of invitation 483 * @param author The author of invitation 484 * @param allowedProfileByModules The rights 485 * @param projectName the name of parent project 486 */ 487 public Invitation(String email, Date date, UserIdentity author, Map<String, String> allowedProfileByModules, String projectName) 488 { 489 _email = email; 490 _date = date; 491 _author = author; 492 _allowedProfileByModules = allowedProfileByModules; 493 _projectName = projectName; 494 } 495 496 /** 497 * Get the email 498 * @return the email 499 */ 500 public String getEmail() 501 { 502 return _email; 503 } 504 505 /** 506 * Get the date of invitation 507 * @return the date of invitation 508 */ 509 public Date getDate() 510 { 511 return _date; 512 } 513 514 /** 515 * Get the author of invitation 516 * @return the author of invitation 517 */ 518 public UserIdentity getAuthor() 519 { 520 return _author; 521 } 522 523 /** 524 * Get the allowed profile for each module 525 * @return the allowed profile for each module 526 */ 527 public Map<String, String> getAllowedProfileByModules() 528 { 529 return _allowedProfileByModules; 530 } 531 532 /** 533 * Get the parent project name 534 * @return the parent project name 535 */ 536 public String getProjectName() 537 { 538 return _projectName; 539 } 540 541 @Override 542 public String toString() 543 { 544 return "Invitation [mail=" + _email + ", project=" + _projectName + "]"; 545 } 546 } 547 548 @Override 549 public Map<String, Object> _getInternalStatistics(Project project, boolean isActive) 550 { 551 if (isActive) 552 { 553 Set<ProjectMember> projectMembers = _projectMemberManager.getProjectMembers(project, true, null); 554 555 if (projectMembers != null) 556 { 557 return Map.of(__MEMBER_NUMBER_HEADER_ID, projectMembers.size()); 558 } 559 else 560 { 561 return Map.of(__MEMBER_NUMBER_HEADER_ID, __SIZE_ERROR); 562 } 563 } 564 else 565 { 566 return Map.of(__MEMBER_NUMBER_HEADER_ID, __SIZE_INACTIVE); 567 } 568 } 569 570 @Override 571 public List<StatisticColumn> _getInternalStatisticModel() 572 { 573 return List.of(new StatisticColumn(__MEMBER_NUMBER_HEADER_ID, new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_STATISTICS_TOOL_COLUMN_MEMBERS")) 574 .withRenderer("Ametys.plugins.workspaces.project.tool.ProjectsGridHelper.renderElements") 575 .withType(StatisticsColumnType.LONG) 576 .withGroup(GROUP_HEADER_ELEMENTS_ID)); 577 } 578 579 @Override 580 protected boolean _showActivatedStatus() 581 { 582 return false; 583 } 584 585 @Override 586 public Set<String> getAllEventTypes() 587 { 588 return Set.of(ObservationConstants.EVENT_MEMBER_ADDED, 589 ObservationConstants.EVENT_MEMBER_JOINED, 590 ObservationConstants.EVENT_MEMBER_DELETED); 591 } 592 593 594 /** 595 * Get the URI of a member in project'site 596 * Implementation does not currently use memberIdentity and type parameters but they could be useful in the future if we want to handle deep links 597 * @param project The project 598 * @param memberIdentity The member identity (user or group) 599 * @param type The member type 600 * @return The member uri 601 */ 602 public String getMemberUri(Project project, String memberIdentity, MemberType type) 603 { 604 return getModuleUrl(project); 605 } 606} 607