PageRenderTime 41ms CodeModel.GetById 12ms RepoModel.GetById 0ms app.codeStats 0ms

/dumbhippo/tags/pre-data-model-stacker/server/src/com/dumbhippo/server/impl/GroupSystemBean.java

https://gitlab.com/manoj-makkuboy/magnetism
Java | 1034 lines | 786 code | 171 blank | 77 comment | 170 complexity | 15a30e0ed96d280a7fbe5f4ca96da63a MD5 | raw file
  1. package com.dumbhippo.server.impl;
  2. import java.io.IOException;
  3. import java.util.ArrayList;
  4. import java.util.Collections;
  5. import java.util.Date;
  6. import java.util.HashSet;
  7. import java.util.List;
  8. import java.util.Set;
  9. import javax.ejb.EJB;
  10. import javax.ejb.Stateless;
  11. import javax.persistence.EntityManager;
  12. import javax.persistence.PersistenceContext;
  13. import javax.persistence.Query;
  14. import org.apache.lucene.search.Hits;
  15. import org.jboss.annotation.IgnoreDependency;
  16. import org.slf4j.Logger;
  17. import com.dumbhippo.GlobalSetup;
  18. import com.dumbhippo.Pair;
  19. import com.dumbhippo.StringUtils;
  20. import com.dumbhippo.TypeUtils;
  21. import com.dumbhippo.dm.ReadWriteSession;
  22. import com.dumbhippo.identity20.Guid;
  23. import com.dumbhippo.identity20.Guid.ParseException;
  24. import com.dumbhippo.live.GroupEvent;
  25. import com.dumbhippo.live.LiveState;
  26. import com.dumbhippo.persistence.Account;
  27. import com.dumbhippo.persistence.AccountClaim;
  28. import com.dumbhippo.persistence.Contact;
  29. import com.dumbhippo.persistence.ContactClaim;
  30. import com.dumbhippo.persistence.Group;
  31. import com.dumbhippo.persistence.GroupAccess;
  32. import com.dumbhippo.persistence.GroupMember;
  33. import com.dumbhippo.persistence.GroupMembershipPolicyRevision;
  34. import com.dumbhippo.persistence.MembershipStatus;
  35. import com.dumbhippo.persistence.Person;
  36. import com.dumbhippo.persistence.Resource;
  37. import com.dumbhippo.persistence.User;
  38. import com.dumbhippo.persistence.Validators;
  39. import com.dumbhippo.search.SearchSystem;
  40. import com.dumbhippo.server.AccountSystem;
  41. import com.dumbhippo.server.Character;
  42. import com.dumbhippo.server.GroupSearchResult;
  43. import com.dumbhippo.server.GroupSystem;
  44. import com.dumbhippo.server.GroupSystemRemote;
  45. import com.dumbhippo.server.IdentitySpider;
  46. import com.dumbhippo.server.NotFoundException;
  47. import com.dumbhippo.server.Notifier;
  48. import com.dumbhippo.server.Pageable;
  49. import com.dumbhippo.server.PersonViewer;
  50. import com.dumbhippo.server.RevisionControl;
  51. import com.dumbhippo.server.dm.DataService;
  52. import com.dumbhippo.server.dm.GroupDMO;
  53. import com.dumbhippo.server.dm.UserClientMatcher;
  54. import com.dumbhippo.server.util.EJBUtil;
  55. import com.dumbhippo.server.views.GroupMemberView;
  56. import com.dumbhippo.server.views.GroupView;
  57. import com.dumbhippo.server.views.PersonView;
  58. import com.dumbhippo.server.views.PersonViewExtra;
  59. import com.dumbhippo.server.views.SystemViewpoint;
  60. import com.dumbhippo.server.views.UserViewpoint;
  61. import com.dumbhippo.server.views.Viewpoint;
  62. @Stateless
  63. public class GroupSystemBean implements GroupSystem, GroupSystemRemote {
  64. @SuppressWarnings("unused")
  65. private static final Logger logger = GlobalSetup.getLogger(GroupSystemBean.class);
  66. @PersistenceContext(unitName = "dumbhippo")
  67. private EntityManager em;
  68. @EJB
  69. private IdentitySpider identitySpider;
  70. @EJB
  71. private PersonViewer personViewer;
  72. @EJB
  73. @IgnoreDependency
  74. private SearchSystem searchSystem;
  75. @EJB
  76. private Notifier notifier;
  77. @EJB
  78. private AccountSystem accountSystem;
  79. @EJB
  80. private RevisionControl revisionControl;
  81. public Group createGroup(User creator, String name, GroupAccess access, String description) {
  82. if (creator == null)
  83. throw new IllegalArgumentException("null group creator");
  84. Group g = new Group(name, access);
  85. g.setDescription(description);
  86. em.persist(g);
  87. GroupMember groupMember = new GroupMember(g, creator.getAccount(), MembershipStatus.ACTIVE);
  88. em.persist(groupMember);
  89. // Fix up the inverse side of the mapping
  90. g.getMembers().add(groupMember);
  91. notifier.onGroupCreated(g);
  92. notifier.onGroupMemberCreated(groupMember, System.currentTimeMillis(), true);
  93. return g;
  94. }
  95. private void invalidateGroupMemberStatus(GroupMember groupMember) {
  96. ReadWriteSession session = DataService.currentSessionRW();
  97. Guid groupId = groupMember.getGroup().getGuid();
  98. session.changed(GroupDMO.class, groupId, "invitedToFollowMembers");
  99. session.changed(GroupDMO.class, groupId, "followerMembers");
  100. session.changed(GroupDMO.class, groupId, "invitedMembers");
  101. session.changed(GroupDMO.class, groupId, "removedMembers");
  102. session.changed(GroupDMO.class, groupId, "activeMembers");
  103. session.changed(GroupDMO.class, groupId, "canSeeMembers");
  104. AccountClaim ac = groupMember.getMember().getAccountClaim();
  105. if (ac != null) {
  106. Guid memberUserId = ac.getOwner().getGuid();
  107. session.changed(GroupDMO.class, groupId, "status", new UserClientMatcher(memberUserId));
  108. }
  109. }
  110. private GroupMember getGroupMemberForUser(Group group, User user, boolean fixupExpected) throws NotFoundException {
  111. List<GroupMember> allMembers = new ArrayList<GroupMember>();
  112. for (GroupMember member : group.getMembers()) {
  113. AccountClaim ac = member.getMember().getAccountClaim();
  114. if (ac != null && ac.getOwner().equals(user)) {
  115. allMembers.add(member);
  116. }
  117. }
  118. if (allMembers.isEmpty())
  119. throw new NotFoundException("GroupMember for user " + user + " in group " + group + " not found");
  120. // if !fixupExpected we could always just return here (assuming allMembers.size() == 1) but for now
  121. // run through the below sanity checking and throw RuntimeException if stuff is wonky
  122. /* We fix up here to always have a single member which is the account member */
  123. MembershipStatus status = MembershipStatus.NONMEMBER;
  124. Set<User> adders = null;
  125. GroupMember accountMember = null;
  126. Account account = user.getAccount();
  127. for (GroupMember member : allMembers) {
  128. // use our "most joined up" status
  129. if (member.getStatus().ordinal() > status.ordinal()) {
  130. status = member.getStatus();
  131. // prefer the "most joined" adders, but prefer any
  132. // adders over no adders
  133. if (!member.getAdders().isEmpty())
  134. adders = member.getAdders();
  135. }
  136. if (member.getMember().equals(account)) {
  137. accountMember = member;
  138. } else {
  139. if (fixupExpected) {
  140. // get rid of anything that isn't an account member
  141. logger.debug("Removing group member {} in favor of account member", member);
  142. // update the flag on account in case new invitations are awaiting
  143. // this is useful when a person signs in for the first time and gets an account,
  144. // we want them to see that they have new group invitations
  145. if (member.getStatus() == MembershipStatus.INVITED) {
  146. account.touchGroupInvitationReceived();
  147. }
  148. group.getMembers().remove(member);
  149. em.remove(member);
  150. } else {
  151. logger.debug("fixup would remove {}", member);
  152. throw new RuntimeException("Unexpected need to fixup GroupMember for user " + user + " in group " + group);
  153. }
  154. }
  155. }
  156. if (status == MembershipStatus.NONMEMBER) {
  157. throw new RuntimeException("MembershipStatus.NONMEMBER found in the database for group " + group + " user " + user);
  158. }
  159. if (accountMember == null) {
  160. if (!fixupExpected) {
  161. throw new RuntimeException("Fixups not expected and group member was not an account member: " + allMembers);
  162. }
  163. logger.debug("Adding new account member to group {} for account {}", group, account);
  164. accountMember = new GroupMember(group, account, status);
  165. if (adders != null && !adders.isEmpty())
  166. accountMember.setAdders(adders);
  167. em.persist(accountMember);
  168. group.getMembers().add(accountMember);
  169. invalidateGroupMemberStatus(accountMember);
  170. notifier.onGroupMemberCreated(accountMember, System.currentTimeMillis(), true);
  171. }
  172. return accountMember;
  173. }
  174. private GroupMember getGroupMemberForContact(Group group, Contact contact) throws NotFoundException {
  175. // FIXME this isn't really going to work well if there are multiple GroupMember, but the
  176. // UI doesn't really offer a way to do that right now anyway I don't think
  177. // This would work fine by itself without the above code, if there
  178. // were never anything that needed fixup
  179. for (GroupMember member : group.getMembers()) {
  180. for (ContactClaim cc : contact.getResources()) {
  181. if (cc.getResource().equals(member.getMember()))
  182. return member;
  183. // Because we don't fixup ContactClaim to point to the Account instead of the
  184. // EmailResource when someone you invited joins, we need an additional
  185. // check here to see if the EmailResource is claimed by a user who is
  186. // a group member.
  187. if (!(cc.getResource() instanceof Account)) {
  188. AccountClaim ac = cc.getResource().getAccountClaim();
  189. if (ac != null && ac.getOwner().getAccount().equals(member.getMember())) {
  190. return member;
  191. }
  192. }
  193. }
  194. }
  195. throw new NotFoundException("GroupMember for contact " + contact + " not found");
  196. }
  197. private GroupMember getGroupMember(Group group, Person person) throws NotFoundException {
  198. if (person instanceof User)
  199. return getGroupMemberForUser(group, (User)person, true);
  200. else
  201. return getGroupMemberForContact(group, (Contact)person);
  202. }
  203. public GroupMember getGroupMember(Group group, Resource resource) throws NotFoundException {
  204. if (resource instanceof Account)
  205. return getGroupMemberForUser(group, ((Account)resource).getOwner(), true);
  206. AccountClaim ac = resource.getAccountClaim();
  207. if (ac != null)
  208. return getGroupMemberForUser(group, ac.getOwner(), true);
  209. for (GroupMember member : group.getMembers()) {
  210. if (member.getMember().equals(resource))
  211. return member;
  212. }
  213. throw new NotFoundException("GroupMember for resource " + resource + " not found");
  214. }
  215. public GroupMember getGroupMember(Group group, Guid resourceId) throws NotFoundException {
  216. for (GroupMember member : group.getMembers()) {
  217. if (member.getMember().getGuid().equals(resourceId))
  218. return member;
  219. }
  220. throw new NotFoundException("GroupMember for resource " + resourceId + " not found");
  221. }
  222. public Set<Group> getInvitedToGroups(UserViewpoint adder, Resource invitee) {
  223. Set<Group> invitedToGroups = new HashSet<Group>();
  224. Set<Group> adderGroups =
  225. findRawGroups(adder, adder.getViewer(), MembershipStatus.ACTIVE);
  226. for (Group group : adderGroups) {
  227. try {
  228. GroupMember member = getGroupMember(group, invitee);
  229. if (member.getAdders().contains(adder)) {
  230. invitedToGroups.add(group);
  231. }
  232. } catch (NotFoundException e) {
  233. // adder is not a member of this group, nothing to do
  234. }
  235. }
  236. return invitedToGroups;
  237. }
  238. public boolean canSeeContent(Viewpoint viewpoint, Group group) {
  239. if (group.getAccess() != GroupAccess.SECRET || viewpoint instanceof SystemViewpoint) {
  240. return true;
  241. } else if (viewpoint instanceof UserViewpoint) {
  242. return getViewerStatus(viewpoint, group).getCanSeeSecretContent();
  243. } else {
  244. return false;
  245. }
  246. }
  247. public boolean canAddMembers(User adder, Group group) {
  248. GroupMember adderMember;
  249. try {
  250. adderMember = getGroupMember(group, adder);
  251. return adderMember.getStatus().getCanAddMembers();
  252. } catch (NotFoundException e) {
  253. return false;
  254. }
  255. }
  256. private void addMember(User adder, Group group, Resource resource, boolean openGroupAdjustment) {
  257. if (openGroupAdjustment && group.getAccess() != GroupAccess.PUBLIC)
  258. throw new RuntimeException("Open group membership adjustment can only be applied to an open group");
  259. if (!openGroupAdjustment && adder == null)
  260. throw new RuntimeException("Adder can only be null when we are doing membership adjustments for an open group");
  261. boolean notifyGroupMembers = !openGroupAdjustment;
  262. GroupMember groupMember;
  263. boolean selfAdd = false;
  264. if (resource instanceof Account && adder != null) {
  265. selfAdd = adder.equals(((Account)resource).getOwner());
  266. }
  267. boolean canAddSelf = false;
  268. try {
  269. groupMember = getGroupMember(group, resource);
  270. canAddSelf = (groupMember.getStatus().ordinal() >= MembershipStatus.REMOVED.ordinal()
  271. || group.getAccess() == GroupAccess.PUBLIC);
  272. } catch (NotFoundException e) {
  273. groupMember = null;
  274. canAddSelf = (group.getAccess() == GroupAccess.PUBLIC);
  275. }
  276. boolean adderCanAdd = true;
  277. if (adder != null)
  278. adderCanAdd = canAddMembers(adder, group);
  279. boolean addAdder = (adder != null && !selfAdd);
  280. long now = System.currentTimeMillis();
  281. MembershipStatus oldStatus;
  282. if (groupMember != null)
  283. oldStatus = groupMember.getStatus();
  284. else
  285. oldStatus = MembershipStatus.NONMEMBER;
  286. // Compute the best status that the adder can make the user
  287. MembershipStatus newStatus;
  288. if (selfAdd)
  289. newStatus = canAddSelf ? MembershipStatus.ACTIVE : MembershipStatus.FOLLOWER;
  290. else
  291. newStatus = adderCanAdd ? MembershipStatus.INVITED : MembershipStatus.INVITED_TO_FOLLOW;
  292. // If that's worse than the current status, then use the current status
  293. if (newStatus.worseThan(oldStatus))
  294. newStatus = oldStatus;
  295. if (groupMember != null) {
  296. switch (groupMember.getStatus()) {
  297. case NONMEMBER:
  298. throw new IllegalStateException();
  299. case FOLLOWER:
  300. // Followers always transition directly to ACTIVE, we don't
  301. // want a 3 way handshake.
  302. if (newStatus == MembershipStatus.INVITED)
  303. newStatus = MembershipStatus.ACTIVE;
  304. break;
  305. case INVITED_TO_FOLLOW:
  306. if (addAdder)
  307. groupMember.addAdder(adder);
  308. break;
  309. case REMOVED:
  310. if (addAdder) {
  311. groupMember.addAdder(adder); // Mark adder for "please come back"
  312. // We don't want to change REMOVED status except by the user
  313. // who was removed
  314. return;
  315. }
  316. break;
  317. case INVITED:
  318. if (addAdder) {
  319. // already invited, add the new adder, let the block be restacked
  320. groupMember.addAdder(adder);
  321. }
  322. break;
  323. case ACTIVE:
  324. return; // Nothing to do
  325. }
  326. groupMember.setStatus(newStatus);
  327. notifier.onGroupMemberStatusChanged(groupMember, now, notifyGroupMembers);
  328. } else {
  329. groupMember = new GroupMember(group, resource, newStatus);
  330. if (addAdder) {
  331. groupMember.addAdder(adder);
  332. // Adding to a group for the first time, touch their last
  333. // added date
  334. if (resource instanceof Account) {
  335. Account acct = (Account) resource;
  336. acct.touchGroupInvitationReceived();
  337. }
  338. }
  339. em.persist(groupMember);
  340. group.getMembers().add(groupMember);
  341. em.persist(group);
  342. notifier.onGroupMemberCreated(groupMember, now, notifyGroupMembers);
  343. }
  344. invalidateGroupMemberStatus(groupMember);
  345. LiveState.getInstance().queueUpdate(new GroupEvent(group.getGuid(), groupMember.getMember().getGuid(),
  346. GroupEvent.Detail.MEMBERS_CHANGED));
  347. }
  348. public void addMember(User adder, Group group, Resource resource) {
  349. addMember(adder, group, resource, false);
  350. }
  351. public void addMember(User adder, Group group, Person person) {
  352. Resource resource = identitySpider.getBestResource(person);
  353. addMember(adder, group, resource, false);
  354. }
  355. public void reviseGroupMembershipPolicy(UserViewpoint revisor, Group group, boolean open) {
  356. // FIXME this is totally screwy, since we no longer have a single magic character
  357. // (we use GNOME sometimes)
  358. User mugshot = accountSystem.getCharacter(Character.MUGSHOT);
  359. Viewpoint viewpoint;
  360. if (revisor.isOfUser(mugshot))
  361. viewpoint = SystemViewpoint.getInstance();
  362. else
  363. viewpoint = revisor;
  364. if (!(viewpoint instanceof SystemViewpoint || canEditGroup(revisor, group)))
  365. throw new RuntimeException("Only active members or Mugshot can edit a group");
  366. if (group.getAccess() == GroupAccess.SECRET)
  367. throw new RuntimeException("Only public groups can have their membership policy changed");
  368. boolean needToInviteFollowers = (group.getAccess() != GroupAccess.PUBLIC && open);
  369. group.setAccess(open ? GroupAccess.PUBLIC : GroupAccess.PUBLIC_INVITE);
  370. DataService.currentSessionRW().changed(GroupDMO.class, group.getGuid(), "isPublic");
  371. int followers = -1;
  372. int invitedFollowers = -1;
  373. if (needToInviteFollowers) {
  374. // we need to make all of the groups' followers members,
  375. // those notifications will not be pushed to group members' stacks
  376. Pair<Integer, Integer> followerCounts = inviteAllFollowers(viewpoint, revisor.getViewer(), group);
  377. followers = followerCounts.getFirst();
  378. invitedFollowers = followerCounts.getSecond();
  379. }
  380. revisionControl.persistRevision(new GroupMembershipPolicyRevision(revisor.getViewer(), group, new Date(), open, followers, invitedFollowers));
  381. }
  382. public Pair<Integer, Integer> inviteAllFollowers(Viewpoint viewpoint, User adder, Group group) {
  383. boolean openGroupAdjustment = (group.getAccess() == GroupAccess.PUBLIC);
  384. Set<User> followers = getUserMembers(viewpoint, group, MembershipStatus.FOLLOWER);
  385. for (User follower : followers) {
  386. addMember(follower, group, follower.getAccount(), openGroupAdjustment);
  387. }
  388. List<Resource> invitedFollowers = getResourceMembers(viewpoint, group, -1, MembershipStatus.INVITED_TO_FOLLOW);
  389. for (Resource invitedFollower : invitedFollowers) {
  390. addMember(adder, group, invitedFollower, openGroupAdjustment);
  391. }
  392. return new Pair<Integer, Integer>(followers.size(), invitedFollowers.size());
  393. }
  394. public boolean canRemoveInvitation(User remover, GroupMember groupMember) {
  395. if (groupMember.getAdders().contains(remover) && !(groupMember.getMember() instanceof Account)) {
  396. return true;
  397. }
  398. return false;
  399. }
  400. public void removeMember(User remover, Group group, Person person) {
  401. try {
  402. // note that getGroupMember() here does a fixup so we only have one GroupMember which
  403. // canonically points to our account.
  404. GroupMember groupMember = getGroupMember(group, person);
  405. removeMember(remover, groupMember);
  406. } catch (NotFoundException e) {
  407. // nothing to do
  408. }
  409. }
  410. public void removeMember(User remover, GroupMember groupMember) {
  411. User userMember = identitySpider.getUser(groupMember.getMember());
  412. Group group = groupMember.getGroup();
  413. // we let adders remove group members they added that do not have an account
  414. if (!remover.equals(userMember) && canRemoveInvitation(remover, groupMember)) {
  415. groupMember.removeAdder(remover);
  416. // entirely remove the group member if the last adder removed them
  417. if (groupMember.getAdders().isEmpty()) {
  418. group.getMembers().remove(groupMember);
  419. em.remove(groupMember);
  420. }
  421. } else if (!remover.equals(userMember)) {
  422. throw new IllegalArgumentException("a group member can only be removed by themself " +
  423. "or by one of its adders if the group member doesn't have an account");
  424. }
  425. // REMOVED has more rights than FOLLOWER so be sure we don't let followers "remove" themselves.
  426. if (groupMember.getStatus().ordinal() > MembershipStatus.REMOVED.ordinal()) {
  427. groupMember.setStatus(MembershipStatus.REMOVED);
  428. notifier.onGroupMemberStatusChanged(groupMember, System.currentTimeMillis(), true);
  429. invalidateGroupMemberStatus(groupMember);
  430. LiveState.getInstance().queueUpdate(new GroupEvent(group.getGuid(),
  431. groupMember.getMember().getGuid(), GroupEvent.Detail.MEMBERS_CHANGED));
  432. } else if (groupMember.getStatus().ordinal() < MembershipStatus.REMOVED.ordinal()) {
  433. // To go from FOLLOWER or INVITED_TO_FOLLOW to removed, we delete the GroupMember
  434. group.getMembers().remove(groupMember);
  435. em.remove(groupMember);
  436. // we don't stackGroupMember here, we only care about transitions to REMOVED for timestamp
  437. // updating (right now anyway)
  438. invalidateGroupMemberStatus(groupMember);
  439. LiveState.getInstance().queueUpdate(new GroupEvent(group.getGuid(),
  440. groupMember.getMember().getGuid(), GroupEvent.Detail.MEMBERS_CHANGED));
  441. } else {
  442. // status == REMOVED, nothing to do
  443. }
  444. }
  445. // this checks if the viewer can see the group activity or its members
  446. private static final String CAN_SEE =
  447. " (g.access >= " + GroupAccess.PUBLIC_INVITE.ordinal() + " OR " +
  448. "EXISTS (SELECT vgm FROM GroupMember vgm, AccountClaim ac " +
  449. "WHERE vgm.group = g AND ac.resource = vgm.member AND ac.owner = :viewer AND " +
  450. "vgm.status >= " + MembershipStatus.INVITED.ordinal() + ")) ";
  451. private static final String CAN_SEE_ANONYMOUS = " g.access >= " + GroupAccess.PUBLIC_INVITE.ordinal() + " ";
  452. private String getStatusClause(MembershipStatus status) {
  453. return getStatusClause(status, false);
  454. }
  455. private String getStatusClause(MembershipStatus status, boolean allGroups) {
  456. if (status != null) {
  457. return " AND gm.status = " + status.ordinal();
  458. } else if (allGroups) {
  459. return " AND ( gm.status >= " + MembershipStatus.INVITED.ordinal() +
  460. " OR gm.status = " + MembershipStatus.INVITED_TO_FOLLOW.ordinal() +
  461. " OR gm.status = " + MembershipStatus.FOLLOWER.ordinal() + " )";
  462. } else {
  463. return " AND gm.status >= " + MembershipStatus.INVITED.ordinal();
  464. }
  465. }
  466. private String getReceivesClause() {
  467. Set<String> receivesOrdinals = new HashSet<String>();
  468. for (MembershipStatus status : MembershipStatus.values()) {
  469. if (status.getReceivesPosts()) {
  470. receivesOrdinals.add(Integer.toString(status.ordinal()));
  471. }
  472. }
  473. return " AND gm.status in (" + StringUtils.join(receivesOrdinals, ", ") + ") ";
  474. }
  475. private Query buildGetResourceMembersQuery(Viewpoint viewpoint, Group group, MembershipStatus status, boolean isCount) {
  476. StringBuilder queryString = new StringBuilder("SELECT ");
  477. Query q;
  478. if (isCount)
  479. queryString.append("count(gm.member)");
  480. else
  481. queryString.append("gm.member");
  482. queryString.append(" FROM GroupMember gm, Group g " +
  483. "WHERE gm.group = :group AND g = :group");
  484. String statusClause = getStatusClause(status);
  485. if (viewpoint instanceof SystemViewpoint) {
  486. q = em.createQuery(queryString + statusClause);
  487. } else if (viewpoint instanceof UserViewpoint) {
  488. User viewer = ((UserViewpoint)viewpoint).getViewer();
  489. q = em.createQuery(queryString + " AND " + CAN_SEE + statusClause)
  490. .setParameter("viewer", viewer);
  491. } else {
  492. q = em.createQuery(queryString + " AND " + CAN_SEE_ANONYMOUS + statusClause);
  493. }
  494. q.setParameter("group", group);
  495. return q;
  496. }
  497. private List<Resource> getResourceMembers(Viewpoint viewpoint, Group group, int maxResults, MembershipStatus status) {
  498. Query q = buildGetResourceMembersQuery(viewpoint, group, status, false);
  499. if (maxResults >= 0)
  500. q.setMaxResults(maxResults);
  501. @SuppressWarnings("unchecked")
  502. List<Resource> result = q.getResultList();
  503. return result;
  504. }
  505. public int getMembersCount(Viewpoint viewpoint, Group group, MembershipStatus status) {
  506. Query q = buildGetResourceMembersQuery(viewpoint, group, status, true);
  507. Object result = q.getSingleResult();
  508. return ((Number) result).intValue();
  509. }
  510. public Set<GroupMemberView> getMembers(Viewpoint viewpoint, Group group, MembershipStatus status, int maxResults, PersonViewExtra... extras) {
  511. // The subversion history has some code to try doing this with fewer queries;
  512. // but for now keeping it simple
  513. List<Resource> resourceMembers = getResourceMembers(viewpoint, group, maxResults, status);
  514. if (resourceMembers.size() == 0)
  515. return Collections.emptySet();
  516. Set<GroupMemberView> result = new HashSet<GroupMemberView>();
  517. logger.debug("will generate person views for {} resources", resourceMembers.size());
  518. for (Resource r : resourceMembers) {
  519. GroupMemberView groupMemberView = personViewer.getPersonView(viewpoint, r, GroupMemberView.class, extras);
  520. if (viewpoint instanceof UserViewpoint) {
  521. try {
  522. groupMemberView.setViewerCanRemoveInvitation(canRemoveInvitation(((UserViewpoint)viewpoint).getViewer(), getGroupMember(group, r)));
  523. } catch (NotFoundException e) {
  524. throw new RuntimeException("Couldn't find a group member that was returned by getResourceMembers", e);
  525. }
  526. }
  527. result.add(groupMemberView);
  528. }
  529. return result;
  530. }
  531. public Set<User> getUserMembers(Viewpoint viewpoint, Group group) {
  532. return getUserMembers(viewpoint, group, null);
  533. }
  534. public Set<User> getUserMembers(Viewpoint viewpoint, Group group, MembershipStatus status) {
  535. List<Resource> resourceMembers = getResourceMembers(viewpoint, group, -1, status);
  536. Set<User> result = new HashSet<User>();
  537. for (Resource r : resourceMembers) {
  538. User user = identitySpider.getUser(r);
  539. if (user != null)
  540. result.add(user);
  541. }
  542. return result;
  543. }
  544. // get people who should be notified on new followers/members
  545. public Set<User> getMembershipChangeRecipients(Group group) {
  546. return getUserMembers(SystemViewpoint.getInstance(), group, MembershipStatus.ACTIVE);
  547. }
  548. private MembershipStatus getViewerStatus(Viewpoint viewpoint, Group group) {
  549. if (viewpoint instanceof SystemViewpoint) {
  550. // System viewpoint as the same visibility as a member
  551. return MembershipStatus.ACTIVE;
  552. } else if (viewpoint instanceof UserViewpoint) {
  553. User viewer = ((UserViewpoint)viewpoint).getViewer();
  554. try {
  555. GroupMember viewerGroupMember = getGroupMember(group, viewer);
  556. return viewerGroupMember.getStatus();
  557. } catch (NotFoundException e) {
  558. return MembershipStatus.NONMEMBER;
  559. }
  560. } else {
  561. return MembershipStatus.NONMEMBER;
  562. }
  563. }
  564. public GroupMember getGroupMember(Viewpoint viewpoint, Group group, User member) throws NotFoundException {
  565. if (!viewpoint.isOfUser(member) &&
  566. group.getAccess() == GroupAccess.SECRET &&
  567. !getViewerStatus(viewpoint, group).getCanSeeSecretContent())
  568. throw new NotFoundException("GroupMember for user " + member + " not found");
  569. return getGroupMember(group, member);
  570. }
  571. // The selection of Group is only needed for the CAN_SEE checks
  572. private static final String FIND_RAW_GROUPS_QUERY =
  573. "SELECT gm.group FROM GroupMember gm, AccountClaim ac, Group g " +
  574. "WHERE ac.resource = gm.member AND ac.owner = :member AND g = gm.group ";
  575. private static enum GroupFindType {
  576. ANY,
  577. PRIVATE,
  578. PUBLIC
  579. }
  580. private String getGroupFindTypeQuery(GroupFindType type) {
  581. if (type.equals(GroupFindType.PRIVATE))
  582. return " AND g.access = " + GroupAccess.SECRET.ordinal();
  583. else if (type.equals(GroupFindType.PUBLIC))
  584. return " AND g.access >= " + GroupAccess.PUBLIC_INVITE.ordinal();
  585. else
  586. return "";
  587. }
  588. private Set<Group> findRawGroups(Viewpoint viewpoint, User member, MembershipStatus status, GroupFindType groupFindType, boolean receivesOnly) {
  589. Query q;
  590. StringBuilder extraClause = new StringBuilder(getStatusClause(status));
  591. extraClause.append(getGroupFindTypeQuery(groupFindType));
  592. if (receivesOnly)
  593. extraClause.append(getReceivesClause());
  594. if (viewpoint.isOfUser(member) || viewpoint instanceof SystemViewpoint) {
  595. // Special case this for effiency
  596. q = em.createQuery(FIND_RAW_GROUPS_QUERY + extraClause.toString());
  597. } else if (viewpoint instanceof UserViewpoint) {
  598. q = em.createQuery(FIND_RAW_GROUPS_QUERY + " AND " + CAN_SEE + extraClause);
  599. q.setParameter("viewer", ((UserViewpoint)viewpoint).getViewer());
  600. } else {
  601. q = em.createQuery(FIND_RAW_GROUPS_QUERY + " AND " + CAN_SEE_ANONYMOUS + extraClause);
  602. }
  603. q.setParameter("member", member);
  604. Set<Group> ret = new HashSet<Group>();
  605. for (Object o : q.getResultList()) {
  606. ret.add((Group) o);
  607. }
  608. return ret;
  609. }
  610. public Set<Group> findRawGroups(Viewpoint viewpoint, User member, MembershipStatus status) {
  611. return findRawGroups(viewpoint, member, status, GroupFindType.ANY, false);
  612. }
  613. public Set<Group> findRawGroups(Viewpoint viewpoint, User member) {
  614. return findRawGroups(viewpoint, member, null, GroupFindType.ANY, false);
  615. }
  616. public void fixupGroupMemberships(User user) {
  617. Set<Group> groups = findRawGroups(SystemViewpoint.getInstance(), user);
  618. for (Group g : groups) {
  619. GroupMember member;
  620. try {
  621. member = getGroupMemberForUser(g, user, true);
  622. if (!(member.getMember() instanceof Account)) {
  623. // continue to fix up the other memberships rather than crashing completely
  624. logger.error("User " + user + " has not-fixed-up group member " + member + " in group " + g);
  625. }
  626. } catch (NotFoundException e) {
  627. // ignore
  628. }
  629. }
  630. }
  631. private Query buildFindGroupsQuery(Viewpoint viewpoint, User member, boolean isCount, MembershipStatus status, boolean allGroups, GroupFindType groupFindType) {
  632. Query q;
  633. StringBuilder queryStr = new StringBuilder("SELECT ");
  634. boolean ownGroups = viewpoint.isOfUser(member);
  635. if (isCount)
  636. queryStr.append("count(gm)");
  637. else
  638. queryStr.append("gm");
  639. queryStr.append(" FROM GroupMember gm, AccountClaim ac, Group g " +
  640. "WHERE ac.resource = gm.member AND ac.owner = :member AND g = gm.group ");
  641. queryStr.append(getStatusClause(status, allGroups));
  642. queryStr.append(getGroupFindTypeQuery(groupFindType));
  643. if (ownGroups || viewpoint instanceof SystemViewpoint) {
  644. // Special case this for effiency
  645. q = em.createQuery(queryStr.toString());
  646. } else if (viewpoint instanceof UserViewpoint) {
  647. queryStr.append(" AND ");
  648. queryStr.append(CAN_SEE);
  649. q = em.createQuery(queryStr.toString());
  650. q.setParameter("viewer", ((UserViewpoint)viewpoint).getViewer());
  651. } else {
  652. queryStr.append(" AND ");
  653. queryStr.append(CAN_SEE_ANONYMOUS);
  654. q = em.createQuery(queryStr.toString());
  655. }
  656. q.setParameter("member", member);
  657. return q;
  658. }
  659. private Query buildFindGroupsQuery(Viewpoint viewpoint, Resource resource) {
  660. Query q;
  661. StringBuilder queryStr = new StringBuilder(" SELECT gm FROM GroupMember gm, Group g " +
  662. "WHERE gm.member = :member AND g = gm.group ");
  663. if (viewpoint instanceof SystemViewpoint) {
  664. // Special case this for effiency
  665. q = em.createQuery(queryStr.toString());
  666. } else if (viewpoint instanceof UserViewpoint) {
  667. queryStr.append(" AND ");
  668. queryStr.append(CAN_SEE);
  669. q = em.createQuery(queryStr.toString());
  670. q.setParameter("viewer", ((UserViewpoint)viewpoint).getViewer());
  671. } else {
  672. queryStr.append(" AND ");
  673. queryStr.append(CAN_SEE_ANONYMOUS);
  674. q = em.createQuery(queryStr.toString());
  675. }
  676. q.setParameter("member", resource);
  677. return q;
  678. }
  679. public int findGroupsCount(Viewpoint viewpoint, User member, MembershipStatus status) {
  680. Query q = buildFindGroupsQuery(viewpoint, member, true, status, false, GroupFindType.ANY);
  681. Object result = q.getSingleResult();
  682. return ((Number) result).intValue();
  683. }
  684. public Set<GroupView> findGroups(Viewpoint viewpoint, User member) {
  685. return findGroups(viewpoint, member, null);
  686. }
  687. public Set<GroupView> findGroups(Viewpoint viewpoint, User member, MembershipStatus status) {
  688. boolean ownGroups = viewpoint.isOfUser(member);
  689. Set<GroupView> result = new HashSet<GroupView>();
  690. Query q = buildFindGroupsQuery(viewpoint, member, false, status, false, GroupFindType.ANY);
  691. for (Object o : q.getResultList()) {
  692. GroupMember groupMember = (GroupMember)o;
  693. Set<PersonView> inviters = new HashSet<PersonView>();
  694. // Only get the inviter information for viewing the viewpoint's own groups
  695. if (ownGroups) {
  696. if (groupMember.getStatus() == MembershipStatus.INVITED) {
  697. Set<User> adders = groupMember.getAdders();
  698. for(User adder : adders) {
  699. inviters.add(personViewer.getPersonView(viewpoint, adder));
  700. }
  701. }
  702. }
  703. result.add(new GroupView(viewpoint, groupMember.getGroup(), groupMember, inviters));
  704. }
  705. return result;
  706. }
  707. public List<GroupMember> findGroups(Viewpoint viewpoint, Resource resource) {
  708. Query q;
  709. AccountClaim ac = resource.getAccountClaim();
  710. if (ac != null) {
  711. // all the group memberships must have been fixed up once
  712. // a user took over a resource
  713. q = buildFindGroupsQuery(viewpoint, ac.getOwner(), false, null, true, GroupFindType.ANY);
  714. } else {
  715. q = buildFindGroupsQuery(viewpoint, resource);
  716. }
  717. return TypeUtils.castList(GroupMember.class, q.getResultList());
  718. }
  719. private static final String FIND_PUBLIC_GROUPS_QUERY =
  720. "FROM Group g WHERE ";
  721. private static final String COUNT_PUBLIC_GROUPS_QUERY =
  722. "SELECT COUNT(g) FROM Group g WHERE ";
  723. public void pagePublicGroups(Viewpoint viewpoint, Pageable<GroupView> pageable) {
  724. Query q;
  725. q = em.createQuery(FIND_PUBLIC_GROUPS_QUERY + CAN_SEE_ANONYMOUS
  726. + " ORDER BY LCASE(g.name)");
  727. q.setFirstResult(pageable.getStart());
  728. q.setMaxResults(pageable.getCount());
  729. List<GroupView> groups = new ArrayList<GroupView>();
  730. for (Object o : q.getResultList()) {
  731. groups.add(new GroupView(viewpoint, (Group) o, null, null));
  732. }
  733. pageable.setResults(groups);
  734. pageable.setTotalCount(getPublicGroupCount());
  735. }
  736. public int getPublicGroupCount() {
  737. Query q = em.createQuery(COUNT_PUBLIC_GROUPS_QUERY + CAN_SEE_ANONYMOUS);
  738. return ((Number) q.getSingleResult()).intValue();
  739. }
  740. public void incrementGroupVersion(final Group group) {
  741. // While it isn't a big deal in practice, the implementation below is slightly
  742. // racy. The following would be better, but triggers a hibernate bug.
  743. //
  744. // em.createQuery("UPDATE Group g set g.version = g.version + 1 WHERE g.id = :id")
  745. // .setParameter("id", groupId)
  746. // .executeUpdate();
  747. //
  748. // em.refresh(group);
  749. group.setVersion(group.getVersion() + 1);
  750. }
  751. // some usages of this don't necessarily trust the groupId to be valid, keep that in mind
  752. public Group lookupGroupById(Viewpoint viewpoint, String groupId) throws NotFoundException {
  753. Guid guid;
  754. try {
  755. guid = new Guid(groupId);
  756. } catch (ParseException e) {
  757. throw new NotFoundException("Invalid group ID " + groupId);
  758. }
  759. return lookupGroupById(viewpoint, guid);
  760. }
  761. public Group lookupGroupById(Viewpoint viewpoint, Guid guid) throws NotFoundException {
  762. Group group = EJBUtil.lookupGuid(em, Group.class, guid);
  763. if (group.getAccess() == GroupAccess.SECRET &&
  764. !getViewerStatus(viewpoint, group).getCanSeeSecretGroup())
  765. throw new NotFoundException("No such group with ID " + guid + " for the given viewpoint");
  766. return group;
  767. }
  768. private static final String CONTACT_IS_MEMBER =
  769. " (EXISTS(SELECT cc FROM ContactClaim cc WHERE cc.contact = contact AND cc.resource = gm.member) OR" +
  770. " EXISTS(SELECT a2 FROM Account a2 WHERE a2.owner IN " +
  771. " (SELECT ac.owner FROM ContactClaim cc, AccountClaim ac WHERE cc.contact = contact AND ac.resource = cc.resource) " +
  772. " AND a2 = gm.member)) ";
  773. // this checks if the user can invite other people to a group
  774. private static final String CAN_SHARE_GROUP =
  775. " (vgm.status = " + MembershipStatus.ACTIVE.ordinal() +
  776. " OR vgm.status = " + MembershipStatus.FOLLOWER.ordinal() + ") ";
  777. // this filters out people who are group members or are already invited, as well
  778. // as filters out followers and people invited to follow if the viewer is a follower
  779. private static final String FIND_ADDABLE_CONTACTS_QUERY =
  780. "SELECT contact from Account a, Contact contact, Group g, GroupMember vgm, AccountClaim ac " +
  781. "WHERE a.owner = :viewer AND contact.account = a AND " +
  782. "g.id = :groupid AND vgm.group = g AND ac.resource = vgm.member AND ac.owner = :viewer AND " + CAN_SHARE_GROUP +
  783. "AND NOT EXISTS(SELECT gm FROM GroupMember gm " +
  784. "WHERE gm.group = :groupid AND " + CONTACT_IS_MEMBER + " AND " +
  785. "(gm.status >= " + MembershipStatus.INVITED.ordinal() +
  786. " OR (vgm.status = " + MembershipStatus.FOLLOWER.ordinal() +
  787. " AND (gm.status = " + MembershipStatus.FOLLOWER.ordinal() +
  788. " OR gm.status = " + MembershipStatus.INVITED_TO_FOLLOW.ordinal() +
  789. "))))";
  790. public Set<PersonView> findAddableContacts(UserViewpoint viewpoint, User owner, Group group, PersonViewExtra... extras) {
  791. Person viewer = viewpoint.getViewer();
  792. if (!viewpoint.isOfUser(owner))
  793. throw new RuntimeException("Not implemented");
  794. Query q = em.createQuery(FIND_ADDABLE_CONTACTS_QUERY);
  795. q.setParameter("viewer", viewer);
  796. q.setParameter("groupid", group.getId());
  797. Set<PersonView> result = new HashSet<PersonView>();
  798. for (Object o: q.getResultList())
  799. result.add(personViewer.getPersonView(viewpoint, (Person)o, extras));
  800. return result;
  801. }
  802. public boolean canEditGroup(UserViewpoint viewpoint, Group group) {
  803. try {
  804. GroupMember member = getGroupMember(group, viewpoint.getViewer());
  805. return member.getStatus() == MembershipStatus.ACTIVE;
  806. } catch (NotFoundException e) {
  807. return false;
  808. }
  809. }
  810. public void setStockPhoto(UserViewpoint viewpoint, Group group, String photo) {
  811. if (!canEditGroup(viewpoint, group))
  812. throw new RuntimeException("Only active members can edit a group");
  813. if (photo != null && !Validators.validateStockPhoto(photo))
  814. throw new RuntimeException("invalid stock photo name");
  815. group.setStockPhoto(photo);
  816. DataService.currentSessionRW().changed(GroupDMO.class, group.getGuid(), "photoUrl");
  817. }
  818. public GroupView loadGroup(Viewpoint viewpoint, Guid guid) throws NotFoundException {
  819. return getGroupView(viewpoint, lookupGroupById(viewpoint, guid));
  820. }
  821. public GroupView getGroupView(Viewpoint viewpoint, Group group) {
  822. GroupMember groupMember = null;
  823. // FIXME: add getGroupMemberByGroupId (or replace getGroupMember), so that
  824. // we only do one lookup in the database. Careful: need to propagate
  825. // the handling of REMOVED members from lookupGroupById to getGroupMember
  826. if (viewpoint instanceof UserViewpoint) {
  827. UserViewpoint userView = (UserViewpoint) viewpoint;
  828. try {
  829. groupMember = getGroupMember(viewpoint, group, userView.getViewer());
  830. } catch (NotFoundException e) {
  831. groupMember = new GroupMember(group, userView.getViewer().getAccount(), MembershipStatus.NONMEMBER);
  832. }
  833. } else {
  834. groupMember = new GroupMember(group, null, MembershipStatus.NONMEMBER);
  835. }
  836. return new GroupView(viewpoint, group, groupMember, null);
  837. }
  838. public void acceptInvitation(UserViewpoint viewpoint, Group group) {
  839. // If you view a group you were invited to, you get added; you can leave again and then
  840. // you enter the REMOVED state where you can re-add yourself but don't get auto-added.
  841. addMember(viewpoint.getViewer(), group, viewpoint.getViewer());
  842. }
  843. public GroupSearchResult searchGroups(Viewpoint viewpoint, String queryString) {
  844. final String[] fields = { "name", "description" };
  845. try {
  846. Hits hits = searchSystem.search(Group.class, fields, queryString);
  847. return new GroupSearchResult(hits);
  848. } catch (org.apache.lucene.queryParser.ParseException e) {
  849. return new GroupSearchResult("Can't parse query '" + queryString + "'");
  850. } catch (IOException e) {
  851. return new GroupSearchResult("System error while searching, please try again");
  852. }
  853. }
  854. public List<GroupView> getGroupSearchGroups(Viewpoint viewpoint, GroupSearchResult searchResult, int start, int count) {
  855. // The efficiency gain of having this wrapper is that we pass the real
  856. // object to the method rather than the proxy; getGroups() can make many,
  857. // many calls back against the GroupSystem
  858. return searchResult.getGroups(this, viewpoint, start, count);
  859. }
  860. public List<Guid> getAllGroupIds() {
  861. return TypeUtils.castList(Guid.class, em.createQuery("SELECT new com.dumbhippo.identity20.Guid(g.id) FROM Group g").getResultList());
  862. }
  863. }