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