Bring image-management up to speed.
[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.core.Options;
31 import net.pterodactylus.sone.freenet.wot.Identity;
32 import net.pterodactylus.sone.template.SoneAccessor;
33 import net.pterodactylus.util.filter.Filter;
34 import net.pterodactylus.util.logging.Logging;
35 import net.pterodactylus.util.validation.Validation;
36 import freenet.keys.FreenetURI;
37
38 /**
39  * A Sone defines everything about a user: her profile, her status updates, her
40  * replies, her likes and dislikes, etc.
41  * <p>
42  * Operations that modify the Sone need to synchronize on the Sone in question.
43  *
44  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
45  */
46 public class Sone implements Fingerprintable, Comparable<Sone> {
47
48         /** comparator that sorts Sones by their nice name. */
49         public static final Comparator<Sone> NICE_NAME_COMPARATOR = new Comparator<Sone>() {
50
51                 @Override
52                 public int compare(Sone leftSone, Sone rightSone) {
53                         int diff = SoneAccessor.getNiceName(leftSone).compareToIgnoreCase(SoneAccessor.getNiceName(rightSone));
54                         if (diff != 0) {
55                                 return diff;
56                         }
57                         return leftSone.getId().compareToIgnoreCase(rightSone.getId());
58                 }
59
60         };
61
62         /** Filter to remove Sones that have not been downloaded. */
63         public static final Filter<Sone> EMPTY_SONE_FILTER = new Filter<Sone>() {
64
65                 @Override
66                 public boolean filterObject(Sone sone) {
67                         return sone.getTime() != 0;
68                 }
69         };
70
71         /** The logger. */
72         private static final Logger logger = Logging.getLogger(Sone.class);
73
74         /** The ID of this Sone. */
75         private final String id;
76
77         /** The identity of this Sone. */
78         private Identity identity;
79
80         /** The URI under which the Sone is stored in Freenet. */
81         private volatile FreenetURI requestUri;
82
83         /** The URI used to insert a new version of this Sone. */
84         /* This will be null for remote Sones! */
85         private volatile FreenetURI insertUri;
86
87         /** The latest edition of the Sone. */
88         private volatile long latestEdition;
89
90         /** The time of the last inserted update. */
91         private volatile long time;
92
93         /** The profile of this Sone. */
94         private volatile Profile profile = new Profile();
95
96         /** The client used by the Sone. */
97         private volatile Client client;
98
99         /** All friend Sones. */
100         private final Set<String> friendSones = Collections.synchronizedSet(new HashSet<String>());
101
102         /** All posts. */
103         private final Set<Post> posts = Collections.synchronizedSet(new HashSet<Post>());
104
105         /** All replies. */
106         private final Set<Reply> replies = Collections.synchronizedSet(new HashSet<Reply>());
107
108         /** The IDs of all liked posts. */
109         private final Set<String> likedPostIds = Collections.synchronizedSet(new HashSet<String>());
110
111         /** The IDs of all liked replies. */
112         private final Set<String> likedReplyIds = Collections.synchronizedSet(new HashSet<String>());
113
114         /** The albums of this Sone. */
115         private final List<Album> albums = Collections.synchronizedList(new ArrayList<Album>());
116
117         /** Sone-specific options. */
118         private final Options options = new Options();
119
120         /**
121          * Creates a new Sone.
122          *
123          * @param id
124          *            The ID of the Sone
125          */
126         public Sone(String id) {
127                 this.id = id;
128         }
129
130         //
131         // ACCESSORS
132         //
133
134         /**
135          * Returns the identity of this Sone.
136          *
137          * @return The identity of this Sone
138          */
139         public String getId() {
140                 return id;
141         }
142
143         /**
144          * Returns the identity of this Sone.
145          *
146          * @return The identity of this Sone
147          */
148         public Identity getIdentity() {
149                 return identity;
150         }
151
152         /**
153          * Sets the identity of this Sone. The {@link Identity#getId() ID} of the
154          * identity has to match this Sone’s {@link #getId()}.
155          *
156          * @param identity
157          *            The identity of this Sone
158          * @return This Sone (for method chaining)
159          * @throws IllegalArgumentException
160          *             if the ID of the identity does not match this Sone’s ID
161          */
162         public Sone setIdentity(Identity identity) throws IllegalArgumentException {
163                 if (!identity.getId().equals(id)) {
164                         throw new IllegalArgumentException("Identity’s ID does not match Sone’s ID!");
165                 }
166                 this.identity = identity;
167                 return this;
168         }
169
170         /**
171          * Returns the name of this Sone.
172          *
173          * @return The name of this Sone
174          */
175         public String getName() {
176                 return (identity != null) ? identity.getNickname() : null;
177         }
178
179         /**
180          * Returns the request URI of this Sone.
181          *
182          * @return The request URI of this Sone
183          */
184         public FreenetURI getRequestUri() {
185                 return (requestUri != null) ? requestUri.setSuggestedEdition(latestEdition) : null;
186         }
187
188         /**
189          * Sets the request URI of this Sone.
190          *
191          * @param requestUri
192          *            The request URI of this Sone
193          * @return This Sone (for method chaining)
194          */
195         public Sone setRequestUri(FreenetURI requestUri) {
196                 if (this.requestUri == null) {
197                         this.requestUri = requestUri.setKeyType("USK").setDocName("Sone").setMetaString(new String[0]);
198                         return this;
199                 }
200                 if (!this.requestUri.equalsKeypair(requestUri)) {
201                         logger.log(Level.WARNING, "Request URI %s tried to overwrite %s!", new Object[] { requestUri, this.requestUri });
202                         return this;
203                 }
204                 return this;
205         }
206
207         /**
208          * Returns the insert URI of this Sone.
209          *
210          * @return The insert URI of this Sone
211          */
212         public FreenetURI getInsertUri() {
213                 return (insertUri != null) ? insertUri.setSuggestedEdition(latestEdition) : null;
214         }
215
216         /**
217          * Sets the insert URI of this Sone.
218          *
219          * @param insertUri
220          *            The insert URI of this Sone
221          * @return This Sone (for method chaining)
222          */
223         public Sone setInsertUri(FreenetURI insertUri) {
224                 if (this.insertUri == null) {
225                         this.insertUri = insertUri.setKeyType("USK").setDocName("Sone").setMetaString(new String[0]);
226                         return this;
227                 }
228                 if (!this.insertUri.equalsKeypair(insertUri)) {
229                         logger.log(Level.WARNING, "Request URI %s tried to overwrite %s!", new Object[] { insertUri, this.insertUri });
230                         return this;
231                 }
232                 return this;
233         }
234
235         /**
236          * Returns the latest edition of this Sone.
237          *
238          * @return The latest edition of this Sone
239          */
240         public long getLatestEdition() {
241                 return latestEdition;
242         }
243
244         /**
245          * Sets the latest edition of this Sone. If the given latest edition is not
246          * greater than the current latest edition, the latest edition of this Sone
247          * is not changed.
248          *
249          * @param latestEdition
250          *            The latest edition of this Sone
251          */
252         public void setLatestEdition(long latestEdition) {
253                 if (!(latestEdition > this.latestEdition)) {
254                         logger.log(Level.FINE, "New latest edition %d is not greater than current latest edition %d!", new Object[] { latestEdition, this.latestEdition });
255                         return;
256                 }
257                 this.latestEdition = latestEdition;
258         }
259
260         /**
261          * Return the time of the last inserted update of this Sone.
262          *
263          * @return The time of the update (in milliseconds since Jan 1, 1970 UTC)
264          */
265         public long getTime() {
266                 return time;
267         }
268
269         /**
270          * Sets the time of the last inserted update of this Sone.
271          *
272          * @param time
273          *            The time of the update (in milliseconds since Jan 1, 1970 UTC)
274          * @return This Sone (for method chaining)
275          */
276         public Sone setTime(long time) {
277                 this.time = time;
278                 return this;
279         }
280
281         /**
282          * Returns a copy of the profile. If you want to update values in the
283          * profile of this Sone, update the values in the returned {@link Profile}
284          * and use {@link #setProfile(Profile)} to change the profile in this Sone.
285          *
286          * @return A copy of the profile
287          */
288         public synchronized Profile getProfile() {
289                 return new Profile(profile);
290         }
291
292         /**
293          * Sets the profile of this Sone. A copy of the given profile is stored so
294          * that subsequent modifications of the given profile are not reflected in
295          * this Sone!
296          *
297          * @param profile
298          *            The profile to set
299          */
300         public synchronized void setProfile(Profile profile) {
301                 this.profile = new Profile(profile);
302         }
303
304         /**
305          * Returns the client used by this Sone.
306          *
307          * @return The client used by this Sone, or {@code null}
308          */
309         public Client getClient() {
310                 return client;
311         }
312
313         /**
314          * Sets the client used by this Sone.
315          *
316          * @param client
317          *            The client used by this Sone, or {@code null}
318          * @return This Sone (for method chaining)
319          */
320         public Sone setClient(Client client) {
321                 this.client = client;
322                 return this;
323         }
324
325         /**
326          * Returns all friend Sones of this Sone.
327          *
328          * @return The friend Sones of this Sone
329          */
330         public List<String> getFriends() {
331                 List<String> friends = new ArrayList<String>(friendSones);
332                 return friends;
333         }
334
335         /**
336          * Sets all friends of this Sone at once.
337          *
338          * @param friends
339          *            The new (and only) friends of this Sone
340          * @return This Sone (for method chaining)
341          */
342         public Sone setFriends(Collection<String> friends) {
343                 friendSones.clear();
344                 friendSones.addAll(friends);
345                 return this;
346         }
347
348         /**
349          * Returns whether this Sone has the given Sone as a friend Sone.
350          *
351          * @param friendSoneId
352          *            The ID of the Sone to check for
353          * @return {@code true} if this Sone has the given Sone as a friend,
354          *         {@code false} otherwise
355          */
356         public boolean hasFriend(String friendSoneId) {
357                 return friendSones.contains(friendSoneId);
358         }
359
360         /**
361          * Adds the given Sone as a friend Sone.
362          *
363          * @param friendSone
364          *            The friend Sone to add
365          * @return This Sone (for method chaining)
366          */
367         public Sone addFriend(String friendSone) {
368                 if (!friendSone.equals(id)) {
369                         friendSones.add(friendSone);
370                 }
371                 return this;
372         }
373
374         /**
375          * Removes the given Sone as a friend Sone.
376          *
377          * @param friendSoneId
378          *            The ID of the friend Sone to remove
379          * @return This Sone (for method chaining)
380          */
381         public Sone removeFriend(String friendSoneId) {
382                 friendSones.remove(friendSoneId);
383                 return this;
384         }
385
386         /**
387          * Returns the list of posts of this Sone, sorted by time, newest first.
388          *
389          * @return All posts of this Sone
390          */
391         public List<Post> getPosts() {
392                 List<Post> sortedPosts;
393                 synchronized (this) {
394                         sortedPosts = new ArrayList<Post>(posts);
395                 }
396                 Collections.sort(sortedPosts, Post.TIME_COMPARATOR);
397                 return sortedPosts;
398         }
399
400         /**
401          * Sets all posts of this Sone at once.
402          *
403          * @param posts
404          *            The new (and only) posts of this Sone
405          * @return This Sone (for method chaining)
406          */
407         public synchronized Sone setPosts(Collection<Post> posts) {
408                 synchronized (this) {
409                         this.posts.clear();
410                         this.posts.addAll(posts);
411                 }
412                 return this;
413         }
414
415         /**
416          * Adds the given post to this Sone. The post will not be added if its
417          * {@link Post#getSone() Sone} is not this Sone.
418          *
419          * @param post
420          *            The post to add
421          */
422         public synchronized void addPost(Post post) {
423                 if (post.getSone().equals(this) && posts.add(post)) {
424                         logger.log(Level.FINEST, "Adding %s to “%s”.", new Object[] { post, getName() });
425                 }
426         }
427
428         /**
429          * Removes the given post from this Sone.
430          *
431          * @param post
432          *            The post to remove
433          */
434         public synchronized void removePost(Post post) {
435                 if (post.getSone().equals(this)) {
436                         posts.remove(post);
437                 }
438         }
439
440         /**
441          * Returns all replies this Sone made.
442          *
443          * @return All replies this Sone made
444          */
445         public synchronized Set<Reply> getReplies() {
446                 return Collections.unmodifiableSet(replies);
447         }
448
449         /**
450          * Sets all replies of this Sone at once.
451          *
452          * @param replies
453          *            The new (and only) replies of this Sone
454          * @return This Sone (for method chaining)
455          */
456         public synchronized Sone setReplies(Collection<Reply> replies) {
457                 this.replies.clear();
458                 this.replies.addAll(replies);
459                 return this;
460         }
461
462         /**
463          * Adds a reply to this Sone. If the given reply was not made by this Sone,
464          * nothing is added to this Sone.
465          *
466          * @param reply
467          *            The reply to add
468          */
469         public synchronized void addReply(Reply reply) {
470                 if (reply.getSone().equals(this)) {
471                         replies.add(reply);
472                 }
473         }
474
475         /**
476          * Removes a reply from this Sone.
477          *
478          * @param reply
479          *            The reply to remove
480          */
481         public synchronized void removeReply(Reply reply) {
482                 if (reply.getSone().equals(this)) {
483                         replies.remove(reply);
484                 }
485         }
486
487         /**
488          * Returns the IDs of all liked posts.
489          *
490          * @return All liked posts’ IDs
491          */
492         public Set<String> getLikedPostIds() {
493                 return Collections.unmodifiableSet(likedPostIds);
494         }
495
496         /**
497          * Sets the IDs of all liked posts.
498          *
499          * @param likedPostIds
500          *            All liked posts’ IDs
501          * @return This Sone (for method chaining)
502          */
503         public synchronized Sone setLikePostIds(Set<String> likedPostIds) {
504                 this.likedPostIds.clear();
505                 this.likedPostIds.addAll(likedPostIds);
506                 return this;
507         }
508
509         /**
510          * Checks whether the given post ID is liked by this Sone.
511          *
512          * @param postId
513          *            The ID of the post
514          * @return {@code true} if this Sone likes the given post, {@code false}
515          *         otherwise
516          */
517         public boolean isLikedPostId(String postId) {
518                 return likedPostIds.contains(postId);
519         }
520
521         /**
522          * Adds the given post ID to the list of posts this Sone likes.
523          *
524          * @param postId
525          *            The ID of the post
526          * @return This Sone (for method chaining)
527          */
528         public synchronized Sone addLikedPostId(String postId) {
529                 likedPostIds.add(postId);
530                 return this;
531         }
532
533         /**
534          * Removes the given post ID from the list of posts this Sone likes.
535          *
536          * @param postId
537          *            The ID of the post
538          * @return This Sone (for method chaining)
539          */
540         public synchronized Sone removeLikedPostId(String postId) {
541                 likedPostIds.remove(postId);
542                 return this;
543         }
544
545         /**
546          * Returns the IDs of all liked replies.
547          *
548          * @return All liked replies’ IDs
549          */
550         public Set<String> getLikedReplyIds() {
551                 return Collections.unmodifiableSet(likedReplyIds);
552         }
553
554         /**
555          * Sets the IDs of all liked replies.
556          *
557          * @param likedReplyIds
558          *            All liked replies’ IDs
559          * @return This Sone (for method chaining)
560          */
561         public synchronized Sone setLikeReplyIds(Set<String> likedReplyIds) {
562                 this.likedReplyIds.clear();
563                 this.likedReplyIds.addAll(likedReplyIds);
564                 return this;
565         }
566
567         /**
568          * Checks whether the given reply ID is liked by this Sone.
569          *
570          * @param replyId
571          *            The ID of the reply
572          * @return {@code true} if this Sone likes the given reply, {@code false}
573          *         otherwise
574          */
575         public boolean isLikedReplyId(String replyId) {
576                 return likedReplyIds.contains(replyId);
577         }
578
579         /**
580          * Adds the given reply ID to the list of replies this Sone likes.
581          *
582          * @param replyId
583          *            The ID of the reply
584          * @return This Sone (for method chaining)
585          */
586         public synchronized Sone addLikedReplyId(String replyId) {
587                 likedReplyIds.add(replyId);
588                 return this;
589         }
590
591         /**
592          * Removes the given post ID from the list of replies this Sone likes.
593          *
594          * @param replyId
595          *            The ID of the reply
596          * @return This Sone (for method chaining)
597          */
598         public synchronized Sone removeLikedReplyId(String replyId) {
599                 likedReplyIds.remove(replyId);
600                 return this;
601         }
602
603         /**
604          * Returns the albums of this Sone.
605          *
606          * @return The albums of this Sone
607          */
608         public List<Album> getAlbums() {
609                 return Collections.unmodifiableList(albums);
610         }
611
612         /**
613          * Adds an album to this Sone.
614          *
615          * @param album
616          *            The album to add
617          */
618         public synchronized void addAlbum(Album album) {
619                 Validation.begin().isNotNull("Album", album).check().isEqual("Album Owner", album.getSone(), this).check();
620                 albums.add(album);
621         }
622
623         /**
624          * Sets the albums of this Sone.
625          *
626          * @param albums
627          *            The albums of this Sone
628          */
629         public synchronized void setAlbums(Collection<? extends Album> albums) {
630                 Validation.begin().isNotNull("Albums", albums).check();
631                 this.albums.clear();
632                 for (Album album : albums) {
633                         addAlbum(album);
634                 }
635         }
636
637         /**
638          * Removes an album from this Sone.
639          *
640          * @param album
641          *            The album to remove
642          */
643         public synchronized void removeAlbum(Album album) {
644                 Validation.begin().isNotNull("Album", album).check().isEqual("Album Owner", album.getSone(), this).check();
645                 albums.remove(album);
646         }
647
648         /**
649          * Returns Sone-specific options.
650          *
651          * @return The options of this Sone
652          */
653         public Options getOptions() {
654                 return options;
655         }
656
657         //
658         // FINGERPRINTABLE METHODS
659         //
660
661         /**
662          * {@inheritDoc}
663          */
664         @Override
665         public synchronized String getFingerprint() {
666                 StringBuilder fingerprint = new StringBuilder();
667                 fingerprint.append(profile.getFingerprint());
668
669                 fingerprint.append("Posts(");
670                 for (Post post : getPosts()) {
671                         fingerprint.append("Post(").append(post.getId()).append(')');
672                 }
673                 fingerprint.append(")");
674
675                 List<Reply> replies = new ArrayList<Reply>(getReplies());
676                 Collections.sort(replies, Reply.TIME_COMPARATOR);
677                 fingerprint.append("Replies(");
678                 for (Reply reply : replies) {
679                         fingerprint.append("Reply(").append(reply.getId()).append(')');
680                 }
681                 fingerprint.append(')');
682
683                 List<String> likedPostIds = new ArrayList<String>(getLikedPostIds());
684                 Collections.sort(likedPostIds);
685                 fingerprint.append("LikedPosts(");
686                 for (String likedPostId : likedPostIds) {
687                         fingerprint.append("Post(").append(likedPostId).append(')');
688                 }
689                 fingerprint.append(')');
690
691                 List<String> likedReplyIds = new ArrayList<String>(getLikedReplyIds());
692                 Collections.sort(likedReplyIds);
693                 fingerprint.append("LikedReplies(");
694                 for (String likedReplyId : likedReplyIds) {
695                         fingerprint.append("Reply(").append(likedReplyId).append(')');
696                 }
697                 fingerprint.append(')');
698
699 //              fingerprint.append("Albums(");
700 //              for (Album album : albums) {
701 //                      fingerprint.append(album.getFingerprint());
702 //              }
703 //              fingerprint.append(')');
704
705                 return fingerprint.toString();
706         }
707
708         //
709         // INTERFACE Comparable<Sone>
710         //
711
712         /**
713          * {@inheritDoc}
714          */
715         @Override
716         public int compareTo(Sone sone) {
717                 return NICE_NAME_COMPARATOR.compare(this, sone);
718         }
719
720         //
721         // OBJECT METHODS
722         //
723
724         /**
725          * {@inheritDoc}
726          */
727         @Override
728         public int hashCode() {
729                 return id.hashCode();
730         }
731
732         /**
733          * {@inheritDoc}
734          */
735         @Override
736         public boolean equals(Object object) {
737                 if (!(object instanceof Sone)) {
738                         return false;
739                 }
740                 return ((Sone) object).id.equals(id);
741         }
742
743         /**
744          * {@inheritDoc}
745          */
746         @Override
747         public String toString() {
748                 return getClass().getName() + "[identity=" + identity + ",requestUri=" + requestUri + ",insertUri(" + String.valueOf(insertUri).length() + "),friends(" + friendSones.size() + "),posts(" + posts.size() + "),replies(" + replies.size() + ")]";
749         }
750
751 }