Create proper Sone URIs.
[Sone.git] / src / main / java / net / pterodactylus / sone / data / Sone.java
1 /*
2  * FreenetSone - Sone.java - Copyright © 2010 David Roden
3  *
4  * This program is free software: you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation, either version 3 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
16  */
17
18 package net.pterodactylus.sone.data;
19
20 import java.util.ArrayList;
21 import java.util.Collection;
22 import java.util.Collections;
23 import java.util.Comparator;
24 import java.util.HashSet;
25 import java.util.List;
26 import java.util.Set;
27 import java.util.logging.Level;
28 import java.util.logging.Logger;
29
30 import net.pterodactylus.sone.freenet.wot.Identity;
31 import net.pterodactylus.sone.template.SoneAccessor;
32 import net.pterodactylus.util.logging.Logging;
33 import freenet.keys.FreenetURI;
34
35 /**
36  * A Sone defines everything about a user: her profile, her status updates, her
37  * replies, her likes and dislikes, etc.
38  * <p>
39  * Operations that modify the Sone need to synchronize on the Sone in question.
40  *
41  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
42  */
43 public class Sone {
44
45         /** comparator that sorts Sones by their nice name. */
46         public static final Comparator<Sone> NICE_NAME_COMPARATOR = new Comparator<Sone>() {
47
48                 @Override
49                 public int compare(Sone leftSone, Sone rightSone) {
50                         int diff = SoneAccessor.getNiceName(leftSone).compareToIgnoreCase(SoneAccessor.getNiceName(rightSone));
51                         if (diff != 0) {
52                                 return diff;
53                         }
54                         return leftSone.getId().compareToIgnoreCase(rightSone.getId());
55                 }
56
57         };
58
59         /** The logger. */
60         private static final Logger logger = Logging.getLogger(Sone.class);
61
62         /** The identity of this Sone. */
63         private final Identity identity;
64
65         /** The URI under which the Sone is stored in Freenet. */
66         private volatile FreenetURI requestUri;
67
68         /** The URI used to insert a new version of this Sone. */
69         /* This will be null for remote Sones! */
70         private volatile FreenetURI insertUri;
71
72         /** The latest edition of the Sone. */
73         private volatile long latestEdition;
74
75         /** The time of the last inserted update. */
76         private volatile long time;
77
78         /** The profile of this Sone. */
79         private volatile Profile profile;
80
81         /** All friend Sones. */
82         private final Set<Sone> friendSones = Collections.synchronizedSet(new HashSet<Sone>());
83
84         /** All posts. */
85         private final Set<Post> posts = Collections.synchronizedSet(new HashSet<Post>());
86
87         /** All replies. */
88         private final Set<Reply> replies = Collections.synchronizedSet(new HashSet<Reply>());
89
90         /** The IDs of all liked posts. */
91         private final Set<String> likedPostIds = Collections.synchronizedSet(new HashSet<String>());
92
93         /** The IDs of all liked replies. */
94         private final Set<String> likedReplyIds = Collections.synchronizedSet(new HashSet<String>());
95
96         /** Modification count. */
97         private volatile long modificationCounter = 0;
98
99         /**
100          * Creates a new Sone.
101          *
102          * @param identity
103          *            The identity of the Sone
104          */
105         public Sone(Identity identity) {
106                 this.identity = identity;
107         }
108
109         //
110         // ACCESSORS
111         //
112
113         /**
114          * Returns the identity of this Sone.
115          *
116          * @return The identity of this Sone
117          */
118         public String getId() {
119                 return identity.getId();
120         }
121
122         /**
123          * Returns the identity of this Sone.
124          *
125          * @return The identity of this Sone
126          */
127         public Identity getIdentity() {
128                 return identity;
129         }
130
131         /**
132          * Returns the name of this Sone.
133          *
134          * @return The name of this Sone
135          */
136         public String getName() {
137                 return identity.getNickname();
138         }
139
140         /**
141          * Returns the request URI of this Sone.
142          *
143          * @return The request URI of this Sone
144          */
145         public FreenetURI getRequestUri() {
146                 return requestUri.setSuggestedEdition(latestEdition);
147         }
148
149         /**
150          * Sets the request URI of this Sone.
151          *
152          * @param requestUri
153          *            The request URI of this Sone
154          * @return This Sone (for method chaining)
155          */
156         public Sone setRequestUri(FreenetURI requestUri) {
157                 if (this.requestUri == null) {
158                         this.requestUri = requestUri.setDocName("Sone").setMetaString(new String[0]);
159                         return this;
160                 }
161                 if (!this.requestUri.equalsKeypair(requestUri)) {
162                         logger.log(Level.WARNING, "Request URI %s tried to overwrite %s!", new Object[] { requestUri, this.requestUri });
163                         return this;
164                 }
165                 setLatestEdition(requestUri.getEdition());
166                 return this;
167         }
168
169         /**
170          * Returns the insert URI of this Sone.
171          *
172          * @return The insert URI of this Sone
173          */
174         public FreenetURI getInsertUri() {
175                 return insertUri.setSuggestedEdition(latestEdition);
176         }
177
178         /**
179          * Sets the insert URI of this Sone.
180          *
181          * @param insertUri
182          *            The insert URI of this Sone
183          * @return This Sone (for method chaining)
184          */
185         public Sone setInsertUri(FreenetURI insertUri) {
186                 if (this.insertUri == null) {
187                         this.insertUri = insertUri.setDocName("Sone").setMetaString(new String[0]);
188                         return this;
189                 }
190                 if (!this.insertUri.equalsKeypair(insertUri)) {
191                         logger.log(Level.WARNING, "Request URI %s tried to overwrite %s!", new Object[] { insertUri, this.insertUri });
192                         return this;
193                 }
194                 setLatestEdition(insertUri.getEdition());
195                 return this;
196         }
197
198         /**
199          * Returns the latest edition of this Sone.
200          *
201          * @return The latest edition of this Sone
202          */
203         public long getLatestEdition() {
204                 return latestEdition;
205         }
206
207         /**
208          * Sets the latest edition of this Sone. If the given latest edition is not
209          * greater than the current latest edition, the latest edition of this Sone
210          * is not changed.
211          *
212          * @param latestEdition
213          *            The latest edition of this Sone
214          */
215         public void setLatestEdition(long latestEdition) {
216                 if (!(latestEdition > this.latestEdition)) {
217                         logger.log(Level.INFO, "New latest edition %d is not greater than current latest edition %d!", new Object[] { latestEdition, this.latestEdition });
218                         return;
219                 }
220                 this.latestEdition = latestEdition;
221         }
222
223         /**
224          * Return the time of the last inserted update of this Sone.
225          *
226          * @return The time of the update (in milliseconds since Jan 1, 1970 UTC)
227          */
228         public long getTime() {
229                 return time;
230         }
231
232         /**
233          * Sets the time of the last inserted update of this Sone.
234          *
235          * @param time
236          *            The time of the update (in milliseconds since Jan 1, 1970 UTC)
237          * @return This Sone (for method chaining)
238          */
239         public Sone setTime(long time) {
240                 this.time = time;
241                 return this;
242         }
243
244         /**
245          * Returns a copy of the profile. If you want to update values in the
246          * profile of this Sone, update the values in the returned {@link Profile}
247          * and use {@link #setProfile(Profile)} to change the profile in this Sone.
248          *
249          * @return A copy of the profile
250          */
251         public Profile getProfile() {
252                 return new Profile(profile);
253         }
254
255         /**
256          * Sets the profile of this Sone. A copy of the given profile is stored so
257          * that subsequent modifications of the given profile are not reflected in
258          * this Sone!
259          *
260          * @param profile
261          *            The profile to set
262          */
263         public synchronized void setProfile(Profile profile) {
264                 this.profile = new Profile(profile);
265                 modificationCounter++;
266         }
267
268         /**
269          * Returns all friend Sones of this Sone.
270          *
271          * @return The friend Sones of this Sone
272          */
273         public List<Sone> getFriends() {
274                 List<Sone> friends = new ArrayList<Sone>(friendSones);
275                 Collections.sort(friends, new Comparator<Sone>() {
276
277                         @Override
278                         public int compare(Sone leftSone, Sone rightSone) {
279                                 int diff = SoneAccessor.getNiceName(leftSone).compareToIgnoreCase(SoneAccessor.getNiceName(rightSone));
280                                 if (diff != 0) {
281                                         return diff;
282                                 }
283                                 return (int) Math.max(Integer.MIN_VALUE, Math.min(Integer.MAX_VALUE, rightSone.getTime() - leftSone.getTime()));
284                         }
285                 });
286                 return friends;
287         }
288
289         /**
290          * Sets all friends of this Sone at once.
291          *
292          * @param friends
293          *            The new (and only) friends of this Sone
294          * @return This Sone (for method chaining)
295          */
296         public Sone setFriends(Collection<Sone> friends) {
297                 friendSones.clear();
298                 friendSones.addAll(friends);
299                 return this;
300         }
301
302         /**
303          * Returns whether this Sone has the given Sone as a friend Sone.
304          *
305          * @param friendSone
306          *            The friend Sone to check for
307          * @return {@code true} if this Sone has the given Sone as a friend,
308          *         {@code false} otherwise
309          */
310         public boolean hasFriend(Sone friendSone) {
311                 return friendSones.contains(friendSone);
312         }
313
314         /**
315          * Adds the given Sone as a friend Sone.
316          *
317          * @param friendSone
318          *            The friend Sone to add
319          * @return This Sone (for method chaining)
320          */
321         public Sone addFriend(Sone friendSone) {
322                 if (!friendSone.equals(this)) {
323                         friendSones.add(friendSone);
324                 }
325                 return this;
326         }
327
328         /**
329          * Removes the given Sone as a friend Sone.
330          *
331          * @param friendSone
332          *            The friend Sone to remove
333          * @return This Sone (for method chaining)
334          */
335         public Sone removeFriend(Sone friendSone) {
336                 friendSones.remove(friendSone);
337                 return this;
338         }
339
340         /**
341          * Returns the list of posts of this Sone, sorted by time, newest first.
342          *
343          * @return All posts of this Sone
344          */
345         public List<Post> getPosts() {
346                 List<Post> sortedPosts = new ArrayList<Post>(posts);
347                 Collections.sort(sortedPosts, new Comparator<Post>() {
348
349                         @Override
350                         public int compare(Post leftPost, Post rightPost) {
351                                 return (int) Math.max(Integer.MIN_VALUE, Math.min(Integer.MAX_VALUE, rightPost.getTime() - leftPost.getTime()));
352                         }
353
354                 });
355                 return sortedPosts;
356         }
357
358         /**
359          * Sets all posts of this Sone at once.
360          *
361          * @param posts
362          *            The new (and only) posts of this Sone
363          * @return This Sone (for method chaining)
364          */
365         public synchronized Sone setPosts(Collection<Post> posts) {
366                 this.posts.clear();
367                 this.posts.addAll(posts);
368                 modificationCounter++;
369                 return this;
370         }
371
372         /**
373          * Adds the given post to this Sone. The post will not be added if its
374          * {@link Post#getSone() Sone} is not this Sone.
375          *
376          * @param post
377          *            The post to add
378          */
379         public synchronized void addPost(Post post) {
380                 if (post.getSone().equals(this) && posts.add(post)) {
381                         logger.log(Level.FINEST, "Adding %s to “%s”.", new Object[] { post, getName() });
382                         modificationCounter++;
383                 }
384         }
385
386         /**
387          * Removes the given post from this Sone.
388          *
389          * @param post
390          *            The post to remove
391          */
392         public synchronized void removePost(Post post) {
393                 if (post.getSone().equals(this) && posts.remove(post)) {
394                         modificationCounter++;
395                 }
396         }
397
398         /**
399          * Returns all replies this Sone made.
400          *
401          * @return All replies this Sone made
402          */
403         public Set<Reply> getReplies() {
404                 logger.log(Level.FINEST, "Friends of %s: %s", new Object[] { this, friendSones });
405                 return Collections.unmodifiableSet(replies);
406         }
407
408         /**
409          * Sets all replies of this Sone at once.
410          *
411          * @param replies
412          *            The new (and only) replies of this Sone
413          * @return This Sone (for method chaining)
414          */
415         public synchronized Sone setReplies(Collection<Reply> replies) {
416                 this.replies.clear();
417                 this.replies.addAll(replies);
418                 modificationCounter++;
419                 return this;
420         }
421
422         /**
423          * Adds a reply to this Sone. If the given reply was not made by this Sone,
424          * nothing is added to this Sone.
425          *
426          * @param reply
427          *            The reply to add
428          */
429         public synchronized void addReply(Reply reply) {
430                 if (reply.getSone().equals(this) && replies.add(reply)) {
431                         modificationCounter++;
432                 }
433         }
434
435         /**
436          * Removes a reply from this Sone.
437          *
438          * @param reply
439          *            The reply to remove
440          */
441         public synchronized void removeReply(Reply reply) {
442                 if (reply.getSone().equals(this) && replies.remove(reply)) {
443                         modificationCounter++;
444                 }
445         }
446
447         /**
448          * Returns the IDs of all liked posts.
449          *
450          * @return All liked posts’ IDs
451          */
452         public Set<String> getLikedPostIds() {
453                 return Collections.unmodifiableSet(likedPostIds);
454         }
455
456         /**
457          * Sets the IDs of all liked posts.
458          *
459          * @param likedPostIds
460          *            All liked posts’ IDs
461          * @return This Sone (for method chaining)
462          */
463         public synchronized Sone setLikePostIds(Set<String> likedPostIds) {
464                 this.likedPostIds.clear();
465                 this.likedPostIds.addAll(likedPostIds);
466                 modificationCounter++;
467                 return this;
468         }
469
470         /**
471          * Checks whether the given post ID is liked by this Sone.
472          *
473          * @param postId
474          *            The ID of the post
475          * @return {@code true} if this Sone likes the given post, {@code false}
476          *         otherwise
477          */
478         public boolean isLikedPostId(String postId) {
479                 return likedPostIds.contains(postId);
480         }
481
482         /**
483          * Adds the given post ID to the list of posts this Sone likes.
484          *
485          * @param postId
486          *            The ID of the post
487          * @return This Sone (for method chaining)
488          */
489         public synchronized Sone addLikedPostId(String postId) {
490                 if (likedPostIds.add(postId)) {
491                         modificationCounter++;
492                 }
493                 return this;
494         }
495
496         /**
497          * Removes the given post ID from the list of posts this Sone likes.
498          *
499          * @param postId
500          *            The ID of the post
501          * @return This Sone (for method chaining)
502          */
503         public synchronized Sone removeLikedPostId(String postId) {
504                 if (likedPostIds.remove(postId)) {
505                         modificationCounter++;
506                 }
507                 return this;
508         }
509
510         /**
511          * Returns the IDs of all liked replies.
512          *
513          * @return All liked replies’ IDs
514          */
515         public Set<String> getLikedReplyIds() {
516                 return Collections.unmodifiableSet(likedReplyIds);
517         }
518
519         /**
520          * Sets the IDs of all liked replies.
521          *
522          * @param likedReplyIds
523          *            All liked replies’ IDs
524          * @return This Sone (for method chaining)
525          */
526         public synchronized Sone setLikeReplyIds(Set<String> likedReplyIds) {
527                 this.likedReplyIds.clear();
528                 this.likedReplyIds.addAll(likedReplyIds);
529                 modificationCounter++;
530                 return this;
531         }
532
533         /**
534          * Checks whether the given reply ID is liked by this Sone.
535          *
536          * @param replyId
537          *            The ID of the reply
538          * @return {@code true} if this Sone likes the given reply, {@code false}
539          *         otherwise
540          */
541         public boolean isLikedReplyId(String replyId) {
542                 return likedReplyIds.contains(replyId);
543         }
544
545         /**
546          * Adds the given reply ID to the list of replies this Sone likes.
547          *
548          * @param replyId
549          *            The ID of the reply
550          * @return This Sone (for method chaining)
551          */
552         public synchronized Sone addLikedReplyId(String replyId) {
553                 if (likedReplyIds.add(replyId)) {
554                         modificationCounter++;
555                 }
556                 return this;
557         }
558
559         /**
560          * Removes the given post ID from the list of replies this Sone likes.
561          *
562          * @param replyId
563          *            The ID of the reply
564          * @return This Sone (for method chaining)
565          */
566         public synchronized Sone removeLikedReplyId(String replyId) {
567                 if (likedReplyIds.remove(replyId)) {
568                         modificationCounter++;
569                 }
570                 return this;
571         }
572
573         /**
574          * Returns the modification counter.
575          *
576          * @return The modification counter
577          */
578         public synchronized long getModificationCounter() {
579                 return modificationCounter;
580         }
581
582         /**
583          * Sets the modification counter.
584          *
585          * @param modificationCounter
586          *            The new modification counter
587          */
588         public synchronized void setModificationCounter(long modificationCounter) {
589                 this.modificationCounter = modificationCounter;
590         }
591
592         //
593         // OBJECT METHODS
594         //
595
596         /**
597          * {@inheritDoc}
598          */
599         @Override
600         public int hashCode() {
601                 return identity.getId().hashCode();
602         }
603
604         /**
605          * {@inheritDoc}
606          */
607         @Override
608         public boolean equals(Object object) {
609                 if (!(object instanceof Sone)) {
610                         return false;
611                 }
612                 return ((Sone) object).identity.getId().equals(identity.getId());
613         }
614
615         /**
616          * {@inheritDoc}
617          */
618         @Override
619         public String toString() {
620                 return getClass().getName() + "[identity=" + identity + ",requestUri=" + requestUri + ",insertUri=" + insertUri + ",friends(" + friendSones.size() + "),posts(" + posts.size() + "),replies(" + replies.size() + ")]";
621         }
622
623 }