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