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