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