PageRenderTime 51ms CodeModel.GetById 13ms RepoModel.GetById 0ms app.codeStats 1ms

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

https://gitlab.com/manoj-makkuboy/magnetism
Java | 1158 lines | 884 code | 185 blank | 89 comment | 146 complexity | ccb8e0544c878fe83e48c8c0356752eb MD5 | raw file
  1. package com.dumbhippo.server.impl;
  2. import java.net.URL;
  3. import java.util.ArrayList;
  4. import java.util.Collection;
  5. import java.util.Collections;
  6. import java.util.Date;
  7. import java.util.HashSet;
  8. import java.util.Iterator;
  9. import java.util.List;
  10. import java.util.Set;
  11. import javax.ejb.EJB;
  12. import javax.ejb.Stateless;
  13. import javax.persistence.EntityManager;
  14. import javax.persistence.NoResultException;
  15. import javax.persistence.PersistenceContext;
  16. import javax.persistence.Query;
  17. import org.jboss.annotation.IgnoreDependency;
  18. import org.slf4j.Logger;
  19. import com.dumbhippo.GlobalSetup;
  20. import com.dumbhippo.Pair;
  21. import com.dumbhippo.TypeUtils;
  22. import com.dumbhippo.identity20.Guid;
  23. import com.dumbhippo.identity20.Guid.ParseException;
  24. import com.dumbhippo.live.LiveState;
  25. import com.dumbhippo.live.LiveUser;
  26. import com.dumbhippo.live.UserChangedEvent;
  27. import com.dumbhippo.live.UserPrefChangedEvent;
  28. import com.dumbhippo.persistence.Account;
  29. import com.dumbhippo.persistence.AccountClaim;
  30. import com.dumbhippo.persistence.Administrator;
  31. import com.dumbhippo.persistence.AimResource;
  32. import com.dumbhippo.persistence.Contact;
  33. import com.dumbhippo.persistence.ContactClaim;
  34. import com.dumbhippo.persistence.ContactStatus;
  35. import com.dumbhippo.persistence.EmailResource;
  36. import com.dumbhippo.persistence.ExternalAccount;
  37. import com.dumbhippo.persistence.ExternalAccountType;
  38. import com.dumbhippo.persistence.FacebookResource;
  39. import com.dumbhippo.persistence.Group;
  40. import com.dumbhippo.persistence.GuidPersistable;
  41. import com.dumbhippo.persistence.LinkResource;
  42. import com.dumbhippo.persistence.Person;
  43. import com.dumbhippo.persistence.Post;
  44. import com.dumbhippo.persistence.Resource;
  45. import com.dumbhippo.persistence.Sentiment;
  46. import com.dumbhippo.persistence.User;
  47. import com.dumbhippo.persistence.UserBioChangedRevision;
  48. import com.dumbhippo.persistence.ValidationException;
  49. import com.dumbhippo.persistence.Validators;
  50. import com.dumbhippo.persistence.XmppResource;
  51. import com.dumbhippo.server.AccountSystem;
  52. import com.dumbhippo.server.ClaimVerifier;
  53. import com.dumbhippo.server.Configuration;
  54. import com.dumbhippo.server.Enabled;
  55. import com.dumbhippo.server.ExternalAccountSystem;
  56. import com.dumbhippo.server.GroupSystem;
  57. import com.dumbhippo.server.HippoProperty;
  58. import com.dumbhippo.server.IdentitySpider;
  59. import com.dumbhippo.server.IdentitySpiderRemote;
  60. import com.dumbhippo.server.NotFoundException;
  61. import com.dumbhippo.server.Notifier;
  62. import com.dumbhippo.server.PermissionDeniedException;
  63. import com.dumbhippo.server.RevisionControl;
  64. import com.dumbhippo.server.dm.ContactDMO;
  65. import com.dumbhippo.server.dm.DataService;
  66. import com.dumbhippo.server.dm.UserClientMatcher;
  67. import com.dumbhippo.server.dm.UserDMO;
  68. import com.dumbhippo.server.util.EJBUtil;
  69. import com.dumbhippo.server.views.SystemViewpoint;
  70. import com.dumbhippo.server.views.UserViewpoint;
  71. import com.dumbhippo.server.views.Viewpoint;
  72. import com.dumbhippo.tx.RetryException;
  73. import com.dumbhippo.tx.TxCallable;
  74. import com.dumbhippo.tx.TxUtils;
  75. /*
  76. * An implementation of the Identity Spider. It sucks your blood.
  77. * @author walters
  78. */
  79. @Stateless
  80. public class IdentitySpiderBean implements IdentitySpider, IdentitySpiderRemote {
  81. static private final Logger logger = GlobalSetup
  82. .getLogger(IdentitySpider.class);
  83. @PersistenceContext(unitName = "dumbhippo")
  84. private EntityManager em;
  85. @EJB
  86. @IgnoreDependency
  87. private ClaimVerifier claimVerifier;
  88. @EJB
  89. @IgnoreDependency
  90. private GroupSystem groupSystem;
  91. @EJB
  92. @IgnoreDependency
  93. private ExternalAccountSystem externalAccounts;
  94. @EJB
  95. private Configuration config;
  96. @EJB
  97. private Notifier notifier;
  98. @EJB
  99. private RevisionControl revisionControl;
  100. public User lookupUserByEmail(Viewpoint viewpoint, String email) throws NotFoundException {
  101. EmailResource res = lookupEmail(email);
  102. // lookupEmail will normally throw a NotFoundException if the resource is not found,
  103. // so it's not clear in what situation res will be null
  104. if (res == null)
  105. throw new NotFoundException("Resource was null");
  106. return lookupUserByResource(viewpoint, res);
  107. }
  108. public User lookupUserByAim(Viewpoint viewpoint, String aim) throws NotFoundException {
  109. AimResource res = lookupAim(aim);
  110. if (res == null)
  111. throw new NotFoundException("Resource was null");
  112. return lookupUserByResource(viewpoint, res);
  113. }
  114. public User lookupUserByFacebookUserId(Viewpoint viewpoint, String facebookUserId) throws NotFoundException {
  115. FacebookResource res = lookupFacebook(facebookUserId);
  116. if (res == null)
  117. throw new NotFoundException("Resource was null");
  118. return lookupUserByResource(viewpoint, res);
  119. }
  120. public User lookupUserByResource(Viewpoint viewpoint, Resource resource) {
  121. return getUser(resource);
  122. }
  123. public User lookupUser(LiveUser luser) {
  124. return em.find(User.class, luser.getGuid().toString());
  125. }
  126. public User lookupUser(Guid guid) {
  127. return em.find(User.class, guid.toString());
  128. }
  129. public Contact lookupContact(Guid guid) {
  130. return em.find(Contact.class, guid.toString());
  131. }
  132. public <T extends GuidPersistable> T lookupGuidString(Class<T> klass,
  133. String id) throws ParseException, NotFoundException {
  134. if (klass.equals(Post.class) || klass.equals(Group.class))
  135. logger.error("Probable bug: looking up Post/Group should use GroupSystem/PostingBoard to get access controls");
  136. return EJBUtil.lookupGuidString(em, klass, id);
  137. }
  138. public <T extends GuidPersistable> T lookupGuid(Class<T> klass, Guid id)
  139. throws NotFoundException {
  140. if (klass.equals(Post.class) || klass.equals(Group.class))
  141. logger.error("Probable bug: looking up Post/Group should use GroupSystem/PostingBoard to get access controls");
  142. return EJBUtil.lookupGuid(em, klass, id);
  143. }
  144. public <T extends GuidPersistable> Set<T> lookupGuidStrings(Class<T> klass,
  145. Set<String> ids) throws ParseException, NotFoundException {
  146. Set<T> ret = new HashSet<T>();
  147. for (String s : ids) {
  148. T obj = lookupGuidString(klass, s);
  149. ret.add(obj);
  150. }
  151. return ret;
  152. }
  153. public <T extends GuidPersistable> Set<T> lookupGuids(Class<T> klass,
  154. Set<Guid> ids) throws NotFoundException {
  155. Set<T> ret = new HashSet<T>();
  156. for (Guid id : ids) {
  157. T obj = lookupGuid(klass, id);
  158. ret.add(obj);
  159. }
  160. return ret;
  161. }
  162. public EmailResource getEmail(final String emailRaw) throws ValidationException, RetryException {
  163. // well, we could do a little better here with the validation...
  164. final String email = EmailResource.canonicalize(emailRaw);
  165. return TxUtils.runNeedsRetry(new TxCallable<EmailResource>() {
  166. public EmailResource call() {
  167. Query q;
  168. q = em.createQuery("from EmailResource e where e.email = :email");
  169. q.setParameter("email", email);
  170. EmailResource res;
  171. try {
  172. res = (EmailResource) q.getSingleResult();
  173. } catch (NoResultException e) {
  174. res = new EmailResource(email);
  175. em.persist(res);
  176. }
  177. return res;
  178. }
  179. });
  180. }
  181. public AimResource getAim(final String screenNameRaw) throws ValidationException, RetryException {
  182. final String screenName = AimResource.canonicalize(screenNameRaw);
  183. return TxUtils.runNeedsRetry(new TxCallable<AimResource>() {
  184. public AimResource call() {
  185. Query q;
  186. q = em.createQuery("from AimResource a where a.screenName = :name");
  187. q.setParameter("name", screenName);
  188. AimResource res;
  189. try {
  190. res = (AimResource) q.getSingleResult();
  191. } catch (NoResultException e) {
  192. try {
  193. res = new AimResource(screenName);
  194. } catch (ValidationException v) {
  195. throw new RuntimeException(v); // Already validated above
  196. }
  197. em.persist(res);
  198. }
  199. return res;
  200. }
  201. });
  202. }
  203. public XmppResource getXmpp(final String jidRaw) throws ValidationException, RetryException {
  204. final String jid = XmppResource.canonicalize(jidRaw);
  205. return TxUtils.runNeedsRetry(new TxCallable<XmppResource>() {
  206. public XmppResource call() {
  207. Query q;
  208. q = em.createQuery("from XmppResource a where a.jid = :name");
  209. q.setParameter("name", jid);
  210. XmppResource res;
  211. try {
  212. res = (XmppResource) q.getSingleResult();
  213. } catch (NoResultException e) {
  214. try {
  215. res = new XmppResource(jid);
  216. } catch (ValidationException v) {
  217. throw new RuntimeException(v); // Already validated above
  218. }
  219. em.persist(res);
  220. }
  221. return res;
  222. }
  223. });
  224. }
  225. private <T extends Resource> T lookupResourceByName(Class<T> klass, String identifier, String name) throws NotFoundException {
  226. Query q;
  227. String className = klass.getName();
  228. q = em.createQuery("SELECT a FROM " + className + " a WHERE a." + identifier
  229. + " = :name");
  230. q.setParameter("name", name);
  231. T res = null;
  232. try {
  233. res = klass.cast(q.getSingleResult());
  234. } catch (NoResultException e) {
  235. throw new NotFoundException("No such resource");
  236. }
  237. return res;
  238. }
  239. public AimResource lookupAim(String screenName) throws NotFoundException {
  240. try {
  241. screenName = AimResource.canonicalize(screenName);
  242. } catch (ValidationException e) {
  243. throw new NotFoundException("Not a valid AIM address", e);
  244. }
  245. return lookupResourceByName(AimResource.class, "screenName", screenName);
  246. }
  247. public EmailResource lookupEmail(String email) throws NotFoundException {
  248. try {
  249. email = EmailResource.canonicalize(email);
  250. } catch (ValidationException e) {
  251. throw new NotFoundException("Not a valid email address", e);
  252. }
  253. return lookupResourceByName(EmailResource.class, "email", email);
  254. }
  255. public XmppResource lookupXmpp(String jid) throws NotFoundException {
  256. try {
  257. jid = XmppResource.canonicalize(jid);
  258. } catch (ValidationException e) {
  259. throw new NotFoundException("Not a valid XMPP address", e);
  260. }
  261. return lookupResourceByName(XmppResource.class, "jid", jid);
  262. }
  263. public FacebookResource lookupFacebook(String facebookUserId) throws NotFoundException {
  264. try {
  265. facebookUserId = FacebookResource.canonicalize(facebookUserId);
  266. } catch (ValidationException e) {
  267. throw new NotFoundException("Not a valid Facebook user id", e);
  268. }
  269. return lookupResourceByName(FacebookResource.class, "facebookUserId", facebookUserId);
  270. }
  271. public LinkResource lookupLink(final URL url) throws NotFoundException {
  272. return lookupResourceByName(LinkResource.class, "url", url.toExternalForm());
  273. }
  274. public LinkResource getLink(final URL url) {
  275. // Retrying here causes side effects all over the code; the right solution
  276. // is to get rid of LinkResource instead.
  277. // return TxUtils.runNeedsRetry(new TxCallable<LinkResource>() {
  278. // public LinkResource call() {
  279. Query q;
  280. q = em.createQuery("SELECT l FROM LinkResource l WHERE l.url = :url");
  281. q.setParameter("url", url.toExternalForm());
  282. LinkResource res;
  283. try {
  284. res = (LinkResource) q.getSingleResult();
  285. } catch (NoResultException e) {
  286. res = new LinkResource(url.toExternalForm());
  287. em.persist(res);
  288. }
  289. return res;
  290. // }
  291. //
  292. // });
  293. }
  294. private User getUserForContact(Contact contact) {
  295. for (ContactClaim cc : contact.getResources()) {
  296. Resource resource = cc.getResource();
  297. if (resource instanceof Account)
  298. return ((Account) resource).getOwner();
  299. else {
  300. AccountClaim ac = resource.getAccountClaim();
  301. if (ac != null) {
  302. return ac.getOwner();
  303. }
  304. }
  305. }
  306. return null;
  307. }
  308. public User getUser(Person person) {
  309. if (person == null)
  310. throw new IllegalArgumentException("null person in getUser()");
  311. if (person instanceof User)
  312. return (User) person;
  313. else {
  314. // logger.debug("getUser: contact = {}", person);
  315. User user = getUserForContact((Contact) person);
  316. // logger.debug("getUserForContact: user = {}", user);
  317. return user;
  318. }
  319. }
  320. public User getUser(Resource resource) {
  321. if (resource instanceof Account)
  322. return ((Account) resource).getOwner();
  323. else {
  324. AccountClaim ac = resource.getAccountClaim();
  325. if (ac != null)
  326. return ac.getOwner();
  327. return null;
  328. }
  329. }
  330. private void invalidateContactStatus(Guid contacterUserId, Guid contactUserId) {
  331. DataService.currentSessionRW().changed(UserDMO.class, contactUserId, "contactStatus",
  332. new UserClientMatcher(contacterUserId));
  333. }
  334. public void addVerifiedOwnershipClaim(User claimedOwner, Resource res) {
  335. // first be sure it isn't a dup - the db constraints check this too,
  336. // but it's more user friendly to no-op here than to throw a db
  337. // exception
  338. Set<AccountClaim> claims = claimedOwner.getAccountClaims();
  339. for (AccountClaim claim : claims) {
  340. if (claim.getResource().equals(res)) {
  341. logger.debug(
  342. "Found existing claim for {} on {}, not adding again",
  343. claimedOwner, res);
  344. return;
  345. }
  346. }
  347. // Now create the new db claim
  348. res.prepareToSetAccountClaim();
  349. AccountClaim ac = new AccountClaim(claimedOwner, res);
  350. em.persist(ac);
  351. // Update inverse mappings
  352. res.setAccountClaim(ac);
  353. claimedOwner.getAccountClaims().add(ac);
  354. // fix up group memberships
  355. groupSystem.fixupGroupMemberships(claimedOwner);
  356. if (res instanceof EmailResource)
  357. DataService.currentSessionRW().changed(UserDMO.class, claimedOwner.getGuid(), "email");
  358. else if (res instanceof AimResource)
  359. DataService.currentSessionRW().changed(UserDMO.class, claimedOwner.getGuid(), "aim");
  360. else if (res instanceof XmppResource)
  361. DataService.currentSessionRW().changed(UserDMO.class, claimedOwner.getGuid(), "xmpp");
  362. else if (res instanceof FacebookResource)
  363. DataService.currentSessionRW().changed(UserDMO.class, claimedOwner.getGuid(), "facebook");
  364. // People may have listed the newly claimed resource as a contact
  365. Collection<Guid> newContacters = findResourceContacters(res);
  366. if (!newContacters.isEmpty()) {
  367. LiveState.getInstance().invalidateContacters(claimedOwner.getGuid());
  368. for (Guid contacter : newContacters) {
  369. invalidateContactStatus(contacter, claimedOwner.getGuid());
  370. LiveState.getInstance().invalidateContacts(contacter);
  371. }
  372. }
  373. }
  374. public void removeVerifiedOwnershipClaim(UserViewpoint viewpoint,
  375. User owner, Resource res) {
  376. Collection<Guid> oldContacters = findResourceContacters(res);
  377. if (!viewpoint.isOfUser(owner)) {
  378. throw new RuntimeException(
  379. "can only remove your own ownership claims");
  380. }
  381. Set<AccountClaim> claims = owner.getAccountClaims();
  382. if (claims.size() < 2) {
  383. // UI shouldn't let this happen, but we double-check here
  384. throw new RuntimeException(
  385. "you have to keep at least one address on an account");
  386. }
  387. for (AccountClaim claim : claims) {
  388. if (claim.getResource().equals(res)) {
  389. logger.debug("Found claim for {} on {}, removing it", owner,
  390. res);
  391. res.setAccountClaim(null);
  392. claims.remove(claim);
  393. em.remove(claim);
  394. if (res instanceof EmailResource)
  395. DataService.currentSessionRW().changed(UserDMO.class, owner.getGuid(), "email");
  396. else if (res instanceof AimResource)
  397. DataService.currentSessionRW().changed(UserDMO.class, owner.getGuid(), "aim");
  398. else if (res instanceof XmppResource)
  399. DataService.currentSessionRW().changed(UserDMO.class, owner.getGuid(), "xmpp");
  400. else if (res instanceof FacebookResource)
  401. DataService.currentSessionRW().changed(UserDMO.class, owner.getGuid(), "facebook");
  402. // People may have listed resource as a contact
  403. if (!oldContacters.isEmpty()) {
  404. LiveState.getInstance().invalidateContacters(owner.getGuid());
  405. for (Guid contacter : oldContacters) {
  406. invalidateContactStatus(contacter, owner.getGuid());
  407. LiveState.getInstance().invalidateContacts(contacter);
  408. }
  409. }
  410. return;
  411. }
  412. }
  413. logger.warn("No current claims for {} on {}, cancelling pending claims", owner, res);
  414. claimVerifier.cancelClaimToken(owner, res);
  415. }
  416. private Collection<Guid> findResourceContacters(Resource res) {
  417. Query q = em.createQuery("SELECT cc.account.owner.id " +
  418. " FROM ContactClaim cc " +
  419. " WHERE cc.resource = :resource");
  420. q.setParameter("resource", res);
  421. Set<Guid> result = new HashSet<Guid>();
  422. for (String id : TypeUtils.castList(String.class, q.getResultList())) {
  423. try {
  424. result.add(new Guid(id));
  425. } catch (ParseException e) {
  426. logger.error("Bad GUID in database: {}", id);
  427. }
  428. }
  429. return result;
  430. }
  431. private Contact findContactByUser(User owner, User contactUser) throws NotFoundException {
  432. Query q = em.createQuery("SELECT cc.contact " +
  433. " FROM ContactClaim cc, AccountClaim ac " +
  434. " WHERE cc.resource = ac.resource " +
  435. " AND cc.account = :account " +
  436. " AND ac.owner = :contact");
  437. q.setParameter("account", owner.getAccount());
  438. q.setParameter("contact", contactUser);
  439. q.setMaxResults(1);
  440. try {
  441. return (Contact)q.getSingleResult();
  442. } catch (NoResultException e) {
  443. throw new NotFoundException("No contact for user");
  444. }
  445. }
  446. public Contact findContactByResource(User owner, Resource resource) throws NotFoundException {
  447. Query q = em.createQuery("SELECT cc.contact " +
  448. " FROM ContactClaim cc " +
  449. " WHERE cc.account = :account" +
  450. " AND cc.resource = :contact");
  451. q.setParameter("account", owner.getAccount());
  452. q.setParameter("contact", resource);
  453. q.setMaxResults(1);
  454. try {
  455. return (Contact)q.getSingleResult();
  456. } catch (NoResultException e) {
  457. throw new NotFoundException("No contact for user");
  458. }
  459. }
  460. private Contact doCreateContact(User user, Resource resource) {
  461. logger.debug("Creating contact for user {} with resource {}", user,
  462. resource);
  463. Account account = user.getAccount();
  464. Contact contact = new Contact(account);
  465. em.persist(contact);
  466. addContactResource(contact, resource);
  467. return contact;
  468. }
  469. public Contact createContact(User user, Resource resource) {
  470. if (user == null)
  471. throw new IllegalArgumentException("null contact owner");
  472. if (resource == null)
  473. throw new IllegalArgumentException("null contact resource");
  474. Contact contact;
  475. try {
  476. contact = findContactByResource(user, resource);
  477. } catch (NotFoundException e) {
  478. contact = doCreateContact(user, resource);
  479. }
  480. return contact;
  481. }
  482. public void addContactResource(Contact contact, Resource resource) {
  483. // Updating the inverse mappings is essential since they are cached;
  484. // if we don't update them the second-level cache will contain stale
  485. // data.
  486. // Updating them won't actually update the data in the second-level
  487. // cache for a non-transactional cache; rather it will flag the data to be
  488. // reloaded from the database.
  489. //
  490. ContactClaim cc = new ContactClaim(contact, resource);
  491. em.persist(cc);
  492. contact.getResources().add(cc);
  493. User contactOwner = contact.getAccount().getOwner();
  494. User resourceOwner = getUser(resource);
  495. LiveState liveState = LiveState.getInstance();
  496. liveState.invalidateContacts(contactOwner.getGuid());
  497. if (resourceOwner != null) {
  498. // Things work better (especially for now, when we don't fully
  499. // implement spidering) if contacts own the account resource for
  500. // users, and not just the EmailResource
  501. if (!(resource instanceof Account)) {
  502. cc = new ContactClaim(contact, resourceOwner.getAccount());
  503. em.persist(cc);
  504. contact.getResources().add(cc);
  505. logger.debug(
  506. "Added contact resource {} pointing to account {}",
  507. cc.getContact(), cc.getAccount());
  508. }
  509. invalidateContactStatus(contactOwner.getGuid(), resourceOwner.getGuid());
  510. liveState.invalidateContacters(resourceOwner.getGuid());
  511. }
  512. }
  513. public void removeContactResource(Contact contact, Resource resource) {
  514. User removedUser = null;
  515. for (ContactClaim cc : contact.getResources()) {
  516. if (cc.getResource().equals(resource)) {
  517. logger.debug("removing contact claim {}", cc);
  518. removedUser = getUser(cc.getResource());
  519. contact.getResources().remove(cc);
  520. em.remove(cc);
  521. break;
  522. }
  523. }
  524. LiveState liveState = LiveState.getInstance();
  525. liveState.invalidateContacts(contact.getAccount().getOwner().getGuid());
  526. if (removedUser != null) {
  527. invalidateContactStatus(contact.getAccount().getOwner().getGuid(), removedUser.getGuid());
  528. liveState.invalidateContacters(removedUser.getGuid());
  529. }
  530. // we could now have a 'bare' Contact with an empty set of resources
  531. }
  532. public void addContactPerson(User user, Person contactPerson) {
  533. logger.debug("adding contact " + contactPerson + " to account "
  534. + user.getAccount());
  535. if (contactPerson instanceof Contact) {
  536. // Must be a contact of user, so nothing to do
  537. assert ((Contact) contactPerson).getAccount() == user.getAccount();
  538. } else {
  539. User contactUser = (User) contactPerson;
  540. try {
  541. findContactByUser(user, contactUser);
  542. } catch (NotFoundException e) {
  543. doCreateContact(user, contactUser.getAccount());
  544. }
  545. }
  546. }
  547. public void deleteContactByPerson(User user, Person contactPerson) {
  548. logger.debug("removing contact {} from account {}", contactPerson, user.getAccount());
  549. Contact contact;
  550. if (contactPerson instanceof Contact) {
  551. contact = (Contact) contactPerson;
  552. } else {
  553. try {
  554. contact = findContactByUser(user, (User)contactPerson);
  555. } catch (NotFoundException e) {
  556. logger.debug("User {} not found as a contact", contactPerson);
  557. return;
  558. }
  559. }
  560. deleteContact(user, contact);
  561. }
  562. public void deleteContactByResource(User user, Resource contactResource) {
  563. logger.debug("removing contact {} from account {}", contactResource, user.getAccount());
  564. Contact contact;
  565. try {
  566. contact = findContactByResource(user, contactResource);
  567. deleteContact(user, contact);
  568. } catch (NotFoundException e) {
  569. logger.debug("Resource {} not found as a contact", contactResource);
  570. return;
  571. }
  572. }
  573. public void deleteContact(User user, Contact contact) {
  574. Set<User> removedUsers = new HashSet<User>();
  575. for (ContactClaim cc : contact.getResources()) {
  576. User resourceUser = getUser(cc.getResource());
  577. if (resourceUser != null)
  578. removedUsers.add(resourceUser);
  579. }
  580. em.remove(contact);
  581. logger.debug("contact deleted");
  582. LiveState liveState = LiveState.getInstance();
  583. liveState.invalidateContacts(user.getGuid());
  584. for (User removedUser : removedUsers) {
  585. invalidateContactStatus(user.getGuid(), removedUser.getGuid());
  586. liveState.invalidateContacters(removedUser.getGuid());
  587. }
  588. }
  589. public void setContactStatus(UserViewpoint viewpoint, User contactUser, ContactStatus status) {
  590. User viewer = viewpoint.getViewer();
  591. if (status == ContactStatus.NONCONTACT) {
  592. deleteContactByPerson(viewer, contactUser);
  593. } else {
  594. Contact contact;
  595. try {
  596. contact = findContactByUser(viewer, contactUser);
  597. } catch (NotFoundException e) {
  598. contact = doCreateContact(viewer, contactUser.getAccount());
  599. }
  600. setContactStatus(viewpoint, contact, status);
  601. }
  602. }
  603. public void setContactStatus(UserViewpoint viewpoint, Contact contact, ContactStatus status) {
  604. User viewer = viewpoint.getViewer();
  605. if (status == ContactStatus.NONCONTACT) {
  606. deleteContactByPerson(viewer, contact);
  607. } else {
  608. if (contact.getStatus() != status) {
  609. contact.setStatus(status);
  610. DataService.currentSessionRW().changed(ContactDMO.class, contact.getGuid(), "status");
  611. User contactUser = getUser(contact);
  612. if (contactUser != null) {
  613. invalidateContactStatus(viewer.getGuid(), contactUser.getGuid());
  614. LiveState.getInstance().invalidateContacters(contactUser.getGuid());
  615. }
  616. }
  617. }
  618. }
  619. public void setContactName(UserViewpoint viewpoint, Contact contact, String name) throws PermissionDeniedException {
  620. if (!contact.getAccount().getOwner().equals(viewpoint.getViewer())) {
  621. throw new PermissionDeniedException("Can't change someone else's contact's name");
  622. }
  623. name = name.trim();
  624. contact.setNickname(name);
  625. DataService.currentSessionRW().changed(ContactDMO.class, contact.getGuid(), "name");
  626. }
  627. // get all contacts, even if they have no user
  628. public Set<Guid> computeContacts(Guid userId) {
  629. User user = em.find(User.class, userId.toString());
  630. Query q = em.createQuery("SELECT c.id " +
  631. " FROM Contact c " +
  632. " WHERE c.account = :account");
  633. q.setParameter("account", user.getAccount());
  634. Set<Guid> result = new HashSet<Guid>();
  635. for (String s : TypeUtils.castList(String.class, q.getResultList())) {
  636. try {
  637. result.add(new Guid(s));
  638. } catch (ParseException e) {
  639. throw new RuntimeException("Bad GUID in database");
  640. }
  641. }
  642. return result;
  643. }
  644. // get only contacts that point to a User
  645. public Set<Guid> computeUserContacts(Guid userId) {
  646. User user = em.find(User.class, userId.toString());
  647. Query q = em.createQuery("SELECT ac.owner.id " +
  648. " FROM ContactClaim cc, AccountClaim ac " +
  649. " WHERE cc.resource = ac.resource " +
  650. " AND cc.account = :account");
  651. q.setParameter("account", user.getAccount());
  652. Set<Guid> result = new HashSet<Guid>();
  653. for (String s : TypeUtils.castList(String.class, q.getResultList())) {
  654. try {
  655. result.add(new Guid(s));
  656. } catch (ParseException e) {
  657. throw new RuntimeException("Bad GUID in database");
  658. }
  659. }
  660. return result;
  661. }
  662. // computes which user GUIDs have a contact which points to a resource
  663. // which is proven-owned (AccountClaim) by the passed-in user.
  664. // "People who say they have a ContactStatus with respect to userId"
  665. public Set<Guid> computeContacters(Guid userId) {
  666. Query q = em.createQuery("SELECT cc.account.owner.id " +
  667. " FROM ContactClaim cc, AccountClaim ac " +
  668. " WHERE cc.resource = ac.resource " +
  669. " AND ac.owner.id = :userId");
  670. q.setParameter("userId", userId.toString());
  671. Set<Guid> result = new HashSet<Guid>();
  672. for (String s : TypeUtils.castList(String.class, q.getResultList())) {
  673. try {
  674. result.add(new Guid(s));
  675. } catch (ParseException e) {
  676. throw new RuntimeException("Bad GUID in database");
  677. }
  678. }
  679. return result;
  680. }
  681. // like computeContacters(), but also returns the status that someone has
  682. // assigned to userId. i.e. returns people who have assigned userId a ContactStatus,
  683. // plus that ContactStatus they have assigned.
  684. public List<Pair<Guid,ContactStatus>> computeContactersWithStatus(Guid userId) {
  685. Query q = em.createQuery("SELECT cc.account.owner.id, cc.contact.status " +
  686. " FROM ContactClaim cc, AccountClaim ac " +
  687. " WHERE cc.resource = ac.resource " +
  688. " AND ac.owner.id = :userId");
  689. q.setParameter("userId", userId.toString());
  690. List<Pair<Guid,ContactStatus>> result = new ArrayList<Pair<Guid,ContactStatus>>();
  691. for (Object o : q.getResultList()) {
  692. Object[] os = (Object[])o;
  693. Guid guid;
  694. try {
  695. guid = new Guid((String)os[0]);
  696. } catch (ParseException e) {
  697. throw new RuntimeException("Bad GUID in database");
  698. }
  699. ContactStatus status = (ContactStatus)os[1];
  700. result.add(new Pair<Guid, ContactStatus>(guid, status));
  701. }
  702. return result;
  703. }
  704. public int computeContactsCount(User user) {
  705. Query q = em.createQuery("SELECT COUNT(*) FROM Contact c WHERE c.account = :account");
  706. q.setParameter("account", user.getAccount());
  707. return ((Number)q.getSingleResult()).intValue();
  708. }
  709. public Set<User> getRawUserContacts(Viewpoint viewpoint, User user) {
  710. if (!isViewerSystemOrFriendOf(viewpoint, user))
  711. return Collections.emptySet();
  712. Guid viewedUserId = user.getGuid();
  713. Set<User> ret = new HashSet<User>();
  714. for (Guid guid : LiveState.getInstance().getContacts(viewedUserId)) {
  715. // The user might be listed as their own contact, but avoid revealing that
  716. if (viewedUserId.equals(guid))
  717. continue;
  718. ret.add(em.find(User.class, guid.toString()));
  719. }
  720. return ret;
  721. }
  722. public Set<User> getUsersWhoHaveUserAsContact(Viewpoint viewpoint, User user) {
  723. if (!isViewerSystemOrFriendOf(viewpoint, user))
  724. return Collections.emptySet();
  725. Guid viewedUserId = user.getGuid();
  726. Set<User> ret = new HashSet<User>();
  727. for (Guid guid : LiveState.getInstance().getContacters(viewedUserId)) {
  728. // The user might be listed as their own contact, but avoid revealing that
  729. if (viewedUserId.equals(guid))
  730. continue;
  731. ret.add(em.find(User.class, guid.toString()));
  732. }
  733. return ret;
  734. }
  735. /**
  736. * @param user
  737. * the user we're looking at the contacts of
  738. * @param contactUser
  739. * is this person a contact of user?
  740. * @return true if contactUser is a contact of user
  741. */
  742. private boolean userHasContact(Viewpoint viewpoint, User user, User contactUser) {
  743. LiveState liveState = LiveState.getInstance();
  744. // See if we have either contacts or contacters cached for the relationship
  745. Set<Guid> contacters = liveState.peekContacters(contactUser.getGuid());
  746. if (contacters != null) {
  747. return contacters.contains(user.getGuid());
  748. }
  749. Set<Guid> contacts = liveState.peekContacts(user.getGuid());
  750. if (contacts != null) {
  751. return contacts.contains(contactUser.getGuid());
  752. }
  753. // If neither is cached, we compute one of them; we prefer to cache
  754. // information for the viewer; when neither the user or contactUser is
  755. // the viewer we prefer the contact side of the relationship (somewhat
  756. // abitrarily)
  757. if (viewpoint.isOfUser(contactUser)) {
  758. contacters = liveState.getContacters(contactUser.getGuid());
  759. return contacters.contains(user.getGuid());
  760. } else {
  761. contacts = liveState.getContacts(user.getGuid());
  762. return contacts.contains(contactUser.getGuid());
  763. }
  764. }
  765. public boolean isContact(Viewpoint viewpoint, User user, User contactUser) {
  766. // see if we're allowed to look at who user's contacts are
  767. if (!isViewerSystemOrFriendOf(viewpoint, user))
  768. return false;
  769. // if we can see their contacts, return whether this person is one of them
  770. return userHasContact(viewpoint, user, contactUser);
  771. }
  772. public boolean isViewerSystemOrFriendOf(Viewpoint viewpoint, User user) {
  773. if (viewpoint instanceof SystemViewpoint) {
  774. return true;
  775. } else if (viewpoint instanceof UserViewpoint) {
  776. UserViewpoint userViewpoint = (UserViewpoint) viewpoint;
  777. if (user.equals(userViewpoint.getViewer()))
  778. return true;
  779. return userHasContact(viewpoint, user, userViewpoint.getViewer());
  780. } else {
  781. return false;
  782. }
  783. }
  784. static final String GET_CONTACT_RESOURCES_QUERY = "SELECT cc.resource FROM ContactClaim cc WHERE cc.contact = :contact";
  785. private Resource getFirstContactResource(Contact contact) {
  786. // An invariant we retain in the database is that every contact
  787. // has at least one resource, so we don't need to check for
  788. // NoResultException
  789. return (Resource) em.createQuery(GET_CONTACT_RESOURCES_QUERY)
  790. .setParameter("contact", contact).setMaxResults(1)
  791. .getSingleResult();
  792. }
  793. public Resource getBestResource(Person person) {
  794. User user = getUser(person);
  795. if (user != null)
  796. return user.getAccount();
  797. return getFirstContactResource((Contact) person);
  798. }
  799. private Account getAttachedAccount(User user) {
  800. if (!em.contains(user))
  801. user = lookupUser(user.getGuid());
  802. return user.getAccount();
  803. }
  804. public boolean getAccountDisabled(User user) {
  805. return user.getAccount().isDisabled();
  806. }
  807. public void setAccountDisabled(User user, boolean disabled) {
  808. Account account = getAttachedAccount(user);
  809. if (account.isDisabled() != disabled) {
  810. account.setDisabled(disabled);
  811. logger.debug("Disabled flag toggled to {} on account {}", disabled,
  812. account);
  813. notifier.onAccountDisabledToggled(account);
  814. }
  815. }
  816. public boolean getAccountAdminDisabled(User user) {
  817. return user.getAccount().isAdminDisabled();
  818. }
  819. public void setAccountAdminDisabled(User user, boolean disabled) {
  820. Account account = getAttachedAccount(user);
  821. if (account.isAdminDisabled() != disabled) {
  822. account.setAdminDisabled(disabled);
  823. logger.debug("adminDisabled flag toggled to {} on account {}", disabled,
  824. account);
  825. notifier.onAccountAdminDisabledToggled(account);
  826. }
  827. }
  828. static final String GET_ADMIN_QUERY = "SELECT adm FROM Administrator adm WHERE adm.account = :acct";
  829. public boolean isAdministrator(User user) {
  830. Account acct = getAttachedAccount(user);
  831. if (acct == null)
  832. return false;
  833. boolean noAuthentication = config.getProperty(
  834. HippoProperty.DISABLE_AUTHENTICATION).equals("true");
  835. if (noAuthentication) {
  836. logger
  837. .debug("auth disabled - everyone gets to be an administrator!");
  838. return true;
  839. }
  840. try {
  841. Administrator adm = (Administrator) em.createQuery(GET_ADMIN_QUERY)
  842. .setParameter("acct", acct).getSingleResult();
  843. return adm != null;
  844. } catch (NoResultException e) {
  845. return false;
  846. }
  847. }
  848. public boolean getMusicSharingEnabled(User user, Enabled enabled) {
  849. // we only share your music if your account is enabled, AND music
  850. // sharing is enabled.
  851. // but we return only the music sharing flag here since the two settings
  852. // are distinct
  853. // in the UI. The pref we send to the client is a composite of the two.
  854. Account account = user.getAccount();
  855. Boolean musicSharingEnabled = account.isMusicSharingEnabled();
  856. if (musicSharingEnabled == null)
  857. musicSharingEnabled = AccountSystem.DEFAULT_ENABLE_MUSIC_SHARING;
  858. switch (enabled) {
  859. case RAW_PREFERENCE_ONLY:
  860. return musicSharingEnabled;
  861. case AND_ACCOUNT_IS_ACTIVE:
  862. return account.isActive() && musicSharingEnabled;
  863. }
  864. throw new IllegalArgumentException(
  865. "invalid value for enabled param to getMusicSharingEnabled");
  866. }
  867. public void setMusicSharingEnabled(UserViewpoint viewpoint, boolean enabled) {
  868. Account account = getAttachedAccount(viewpoint.getViewer());
  869. if (account.isMusicSharingEnabled() == null || account.isMusicSharingEnabled() != enabled) {
  870. account.setMusicSharingEnabled(enabled);
  871. notifier.onMusicSharingToggled(viewpoint);
  872. LiveState.getInstance().queueUpdate(new UserPrefChangedEvent(viewpoint.getViewer().getGuid(), "musicSharingEnabled", Boolean.toString(enabled)));
  873. DataService.currentSessionRW().changed(UserDMO.class, account.getOwner().getGuid(), "musicSharingEnabled");
  874. }
  875. }
  876. public boolean getMusicSharingPrimed(User user) {
  877. Account account = getAttachedAccount(user);
  878. return account.isMusicSharingPrimed();
  879. }
  880. public void setMusicSharingPrimed(User user, boolean primed) {
  881. Account account = getAttachedAccount(user);
  882. if (account.isMusicSharingPrimed() != primed) {
  883. account.setMusicSharingPrimed(primed);
  884. LiveState.getInstance().queueUpdate(new UserPrefChangedEvent(user.getGuid(), "musicSharingPrimed", Boolean.toString(primed)));
  885. DataService.currentSessionRW().changed(UserDMO.class, account.getOwner().getGuid(), "musicSharingPrimed");
  886. }
  887. }
  888. public boolean getApplicationUsageEnabled(User user) {
  889. Boolean enabled = user.getAccount().isApplicationUsageEnabled();
  890. return enabled != null ? enabled : AccountSystem.DEFAULT_APPLICATION_USAGE_ENABLED;
  891. }
  892. public void setApplicationUsageEnabled(UserViewpoint viewpoint, boolean enabled) {
  893. Account account = viewpoint.getViewer().getAccount();
  894. boolean wasSet = account.isApplicationUsageEnabled() != null;
  895. boolean wasEnabled = getApplicationUsageEnabled(viewpoint.getViewer());
  896. account.setApplicationUsageEnabled(enabled);
  897. if (enabled != wasEnabled) {
  898. LiveState.getInstance().queueUpdate(new UserPrefChangedEvent(viewpoint.getViewer().getGuid(), "applicationUsageEnabled", Boolean.toString(enabled)));
  899. DataService.currentSessionRW().changed(UserDMO.class, account.getOwner().getGuid(), "applicationUsageEnabled");
  900. }
  901. if (enabled != wasEnabled || !wasSet)
  902. notifier.onApplicationUsageToggled(viewpoint);
  903. }
  904. public void incrementUserVersion(final User user) {
  905. // While it isn't a big deal in practice, the implementation below is
  906. // slightly
  907. // racy. The following would be better, but triggers a hibernate bug.
  908. //
  909. // em.createQuery("UPDATE User u set u.version = u.version + 1 WHERE
  910. // u.id = :id")
  911. // .setParameter("id", userId)
  912. // .executeUpdate();
  913. //
  914. // em.refresh(user);
  915. user.setVersion(user.getVersion() + 1);
  916. }
  917. public void setBio(UserViewpoint viewpoint, User user, String bio) {
  918. if (!viewpoint.isOfUser(user))
  919. throw new RuntimeException("can only set one's own bio");
  920. if (!em.contains(user))
  921. throw new RuntimeException("user not attached");
  922. Account acct = user.getAccount();
  923. acct.setBio(bio);
  924. revisionControl.persistRevision(new UserBioChangedRevision(viewpoint.getViewer(), new Date(), bio));
  925. }
  926. public void setMusicBio(UserViewpoint viewpoint, User user, String bio) {
  927. if (!viewpoint.isOfUser(user))
  928. throw new RuntimeException("can only set one's own music bio");
  929. if (!em.contains(user))
  930. throw new RuntimeException("user not attached");
  931. Account acct = user.getAccount();
  932. acct.setMusicBio(bio);
  933. }
  934. /**
  935. * Note that the photo CAN be null, which means to use the uploaded photo
  936. * for the user instead of a photo filename.
  937. */
  938. public void setStockPhoto(UserViewpoint viewpoint, User user, String photo) {
  939. if (!viewpoint.isOfUser(user))
  940. throw new RuntimeException("can only set one's own photo");
  941. if (photo != null && !Validators.validateStockPhoto(photo))
  942. throw new RuntimeException("invalid stock photo name");
  943. user.setStockPhoto(photo);
  944. DataService.currentSessionRW().changed(UserDMO.class, user.getGuid(), "photoUrl");
  945. LiveState.getInstance().queueUpdate(new UserChangedEvent(user.getGuid(), UserChangedEvent.Detail.PHOTO));
  946. }
  947. public Set<User> getMySpaceContacts(UserViewpoint viewpoint) {
  948. Set<User> contacts = getRawUserContacts(viewpoint, viewpoint.getViewer());
  949. // filter out anyone with no myspace
  950. Iterator<User> iterator = contacts.iterator();
  951. while (iterator.hasNext()) {
  952. User user = iterator.next();
  953. ExternalAccount external;
  954. try {
  955. // not using externalAccounts.getMySpaceName() because we
  956. // also want to check we have the friend id
  957. external = externalAccounts.lookupExternalAccount(
  958. viewpoint, user, ExternalAccountType.MYSPACE);
  959. if (external.getSentiment() == Sentiment.LOVE
  960. && external.getHandle() != null
  961. && external.getExtra() != null) {
  962. // we have myspace name AND friend ID
  963. continue;
  964. }
  965. } catch (NotFoundException e) {
  966. // nothing
  967. }
  968. // remove - did not have a myspace name
  969. iterator.remove();
  970. }
  971. return contacts;
  972. }
  973. }