08a9ac12316b1760ca8478af93aa7660bcf66ba6
[Sone.git] / src / main / java / net / pterodactylus / sone / data / impl / DefaultSone.java
1 /*
2  * Sone - SoneImpl.java - Copyright © 2010–2013 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.impl;
19
20 import static com.google.common.base.Preconditions.checkNotNull;
21
22 import java.util.ArrayList;
23 import java.util.Collection;
24 import java.util.Collections;
25 import java.util.List;
26 import java.util.Set;
27 import java.util.concurrent.CopyOnWriteArraySet;
28 import java.util.logging.Level;
29 import java.util.logging.Logger;
30
31 import net.pterodactylus.sone.core.Options;
32 import net.pterodactylus.sone.data.Album;
33 import net.pterodactylus.sone.data.Client;
34 import net.pterodactylus.sone.data.Post;
35 import net.pterodactylus.sone.data.PostReply;
36 import net.pterodactylus.sone.data.Profile;
37 import net.pterodactylus.sone.data.Reply;
38 import net.pterodactylus.sone.data.Sone;
39 import net.pterodactylus.sone.database.AlbumBuilder;
40 import net.pterodactylus.sone.database.Database;
41 import net.pterodactylus.sone.database.PostBuilder;
42 import net.pterodactylus.sone.database.PostReplyBuilder;
43 import net.pterodactylus.sone.freenet.wot.Identity;
44 import net.pterodactylus.util.logging.Logging;
45
46 import freenet.keys.FreenetURI;
47
48 import com.google.common.base.Optional;
49 import com.google.common.hash.Hasher;
50 import com.google.common.hash.Hashing;
51
52 /**
53  * Dumb, store-everything-in-memory {@link Sone} implementation.
54  *
55  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
56  */
57 public class DefaultSone implements Sone {
58
59         /** The logger. */
60         private static final Logger logger = Logging.getLogger(DefaultSone.class);
61
62         /** The database. */
63         private final Database database;
64
65         /** The ID of this Sone. */
66         private final String id;
67
68         /** Whether the Sone is local. */
69         private final boolean local;
70
71         /** The URI under which the Sone is stored in Freenet. */
72         private volatile FreenetURI requestUri;
73
74         /** The URI used to insert a new version of this Sone. */
75         /* This will be null for remote Sones! */
76         private volatile FreenetURI insertUri;
77
78         /** The latest edition of the Sone. */
79         private volatile long latestEdition;
80
81         /** The time of the last inserted update. */
82         private volatile long time;
83
84         /** The status of this Sone. */
85         private volatile SoneStatus status = SoneStatus.unknown;
86
87         /** The profile of this Sone. */
88         private volatile Profile profile = new Profile(this);
89
90         /** The client used by the Sone. */
91         private final Client client;
92
93         /** Whether this Sone is known. */
94         private volatile boolean known;
95
96         /** All friend Sones. */
97         private final Set<String> friendSones = new CopyOnWriteArraySet<String>();
98
99         /** All posts. */
100         private final Set<Post> posts = new CopyOnWriteArraySet<Post>();
101
102         /** All replies. */
103         private final Set<PostReply> replies = new CopyOnWriteArraySet<PostReply>();
104
105         /** The IDs of all liked posts. */
106         private final Set<String> likedPostIds = new CopyOnWriteArraySet<String>();
107
108         /** The IDs of all liked replies. */
109         private final Set<String> likedReplyIds = new CopyOnWriteArraySet<String>();
110
111         /** The root album containing all albums. */
112         private final Album rootAlbum;
113
114         /** Sone-specific options. */
115         private Options options = new Options();
116
117         /**
118          * Creates a new Sone.
119          *
120          * @param id
121          *              The ID of the Sone
122          * @param local
123          *              {@code true} if the Sone is a local Sone, {@code false} otherwise
124          */
125         public DefaultSone(Database database, String id, boolean local, Client client) {
126                 this.database = database;
127                 this.id = id;
128                 this.local = local;
129                 this.client = client;
130                 rootAlbum = new DefaultAlbumBuilder(database, this, null).build();
131         }
132
133         //
134         // ACCESSORS
135         //
136
137         public String getId() {
138                 return id;
139         }
140
141         public Identity getIdentity() {
142                 return database.getIdentity(id).get();
143         }
144
145         public String getName() {
146                 return getIdentity().getNickname();
147         }
148
149         public boolean isLocal() {
150                 return local;
151         }
152
153         public FreenetURI getRequestUri() {
154                 return (requestUri != null) ? requestUri.setSuggestedEdition(latestEdition) : null;
155         }
156
157         public Sone setRequestUri(FreenetURI requestUri) {
158                 if (this.requestUri == null) {
159                         this.requestUri = requestUri.setKeyType("USK").setDocName("Sone").setMetaString(new String[0]);
160                         return this;
161                 }
162                 if (!this.requestUri.equalsKeypair(requestUri)) {
163                         logger.log(Level.WARNING, String.format("Request URI %s tried to overwrite %s!", requestUri, this.requestUri));
164                         return this;
165                 }
166                 return this;
167         }
168
169         public FreenetURI getInsertUri() {
170                 return (insertUri != null) ? insertUri.setSuggestedEdition(latestEdition) : null;
171         }
172
173         public Sone setInsertUri(FreenetURI insertUri) {
174                 if (this.insertUri == null) {
175                         this.insertUri = insertUri.setKeyType("USK").setDocName("Sone").setMetaString(new String[0]);
176                         return this;
177                 }
178                 if (!this.insertUri.equalsKeypair(insertUri)) {
179                         logger.log(Level.WARNING, String.format("Request URI %s tried to overwrite %s!", insertUri, this.insertUri));
180                         return this;
181                 }
182                 return this;
183         }
184
185         public long getLatestEdition() {
186                 return latestEdition;
187         }
188
189         public void setLatestEdition(long latestEdition) {
190                 if (!(latestEdition > this.latestEdition)) {
191                         logger.log(Level.FINE, String.format("New latest edition %d is not greater than current latest edition %d!", latestEdition, this.latestEdition));
192                         return;
193                 }
194                 this.latestEdition = latestEdition;
195         }
196
197         public long getTime() {
198                 return time;
199         }
200
201         public Sone setTime(long time) {
202                 this.time = time;
203                 return this;
204         }
205
206         public SoneStatus getStatus() {
207                 return status;
208         }
209
210         public Sone setStatus(SoneStatus status) {
211                 this.status = checkNotNull(status, "status must not be null");
212                 return this;
213         }
214
215         public Profile getProfile() {
216                 return new Profile(profile);
217         }
218
219         public void setProfile(Profile profile) {
220                 this.profile = new Profile(profile);
221         }
222
223         public Client getClient() {
224                 return client;
225         }
226
227         public boolean isKnown() {
228                 return known;
229         }
230
231         public Sone setKnown(boolean known) {
232                 this.known = known;
233                 return this;
234         }
235
236         public List<String> getFriends() {
237                 List<String> friends = new ArrayList<String>(friendSones);
238                 return friends;
239         }
240
241         public boolean hasFriend(String friendSoneId) {
242                 return friendSones.contains(friendSoneId);
243         }
244
245         public Sone addFriend(String friendSone) {
246                 if (!friendSone.equals(id)) {
247                         friendSones.add(friendSone);
248                 }
249                 return this;
250         }
251
252         public Sone removeFriend(String friendSoneId) {
253                 friendSones.remove(friendSoneId);
254                 return this;
255         }
256
257         public List<Post> getPosts() {
258                 List<Post> sortedPosts;
259                 synchronized (this) {
260                         sortedPosts = new ArrayList<Post>(posts);
261                 }
262                 Collections.sort(sortedPosts, Post.TIME_COMPARATOR);
263                 return sortedPosts;
264         }
265
266         public Sone setPosts(Collection<Post> posts) {
267                 synchronized (this) {
268                         this.posts.clear();
269                         this.posts.addAll(posts);
270                 }
271                 return this;
272         }
273
274         public void addPost(Post post) {
275                 if (post.getSone().equals(this) && posts.add(post)) {
276                         logger.log(Level.FINEST, String.format("Adding %s to “%s”.", post, getName()));
277                 }
278         }
279
280         public void removePost(Post post) {
281                 if (post.getSone().equals(this)) {
282                         posts.remove(post);
283                 }
284         }
285
286         public Set<PostReply> getReplies() {
287                 return Collections.unmodifiableSet(replies);
288         }
289
290         public Sone setReplies(Collection<PostReply> replies) {
291                 this.replies.clear();
292                 this.replies.addAll(replies);
293                 return this;
294         }
295
296         public void addReply(PostReply reply) {
297                 if (reply.getSone().equals(this)) {
298                         replies.add(reply);
299                 }
300         }
301
302         public void removeReply(PostReply reply) {
303                 if (reply.getSone().equals(this)) {
304                         replies.remove(reply);
305                 }
306         }
307
308         public Set<String> getLikedPostIds() {
309                 return Collections.unmodifiableSet(likedPostIds);
310         }
311
312         public Sone setLikePostIds(Set<String> likedPostIds) {
313                 this.likedPostIds.clear();
314                 this.likedPostIds.addAll(likedPostIds);
315                 return this;
316         }
317
318         public boolean isLikedPostId(String postId) {
319                 return likedPostIds.contains(postId);
320         }
321
322         public Sone removeLikedPostId(String postId) {
323                 likedPostIds.remove(postId);
324                 return this;
325         }
326
327         public Set<String> getLikedReplyIds() {
328                 return Collections.unmodifiableSet(likedReplyIds);
329         }
330
331         public Sone setLikeReplyIds(Set<String> likedReplyIds) {
332                 this.likedReplyIds.clear();
333                 this.likedReplyIds.addAll(likedReplyIds);
334                 return this;
335         }
336
337         public boolean isLikedReplyId(String replyId) {
338                 return likedReplyIds.contains(replyId);
339         }
340
341         public Sone addLikedReplyId(String replyId) {
342                 likedReplyIds.add(replyId);
343                 return this;
344         }
345
346         public Sone removeLikedReplyId(String replyId) {
347                 likedReplyIds.remove(replyId);
348                 return this;
349         }
350
351         public Album getRootAlbum() {
352                 return rootAlbum;
353         }
354
355         public Options getOptions() {
356                 return options;
357         }
358
359         /* TODO - remove this method again, maybe add an option provider */
360         public void setOptions(Options options) {
361                 this.options = options;
362         }
363
364         @Override
365         public AlbumBuilder newAlbumBuilder() {
366                 return new DefaultAlbumBuilder(database, this, rootAlbum.getId());
367         }
368
369         public PostBuilder newPostBuilder() {
370                 return new DefaultPostBuilder(database, getId()) {
371                         @Override
372                         public Post build(Optional<PostCreated> postCreated) {
373                                 Post post = super.build(postCreated);
374                                 database.storePost(post);
375                                 return post;
376                         }
377                 };
378         }
379
380         @Override
381         public PostReplyBuilder newPostReplyBuilder(String postId) throws IllegalStateException {
382                 return new DefaultPostReplyBuilder(database, getId(), postId) {
383                         @Override
384                         public PostReply build(Optional<PostReplyCreated> postReplyCreated) {
385                                 PostReply postReply = super.build(postReplyCreated);
386                                 database.storePostReply(postReply);
387                                 return postReply;
388                         }
389                 };
390         }
391
392         public Modifier modify() {
393                 return new Modifier() {
394                         private long latestEdition = DefaultSone.this.latestEdition;
395                         @Override
396                         public Modifier setLatestEdition(long latestEdition) {
397                                 this.latestEdition = latestEdition;
398                                 return this;
399                         }
400
401                         @Override
402                         public Sone update() {
403                                 DefaultSone.this.latestEdition = latestEdition;
404                                 return DefaultSone.this;
405                         }
406                 };
407         }
408
409         //
410         // FINGERPRINTABLE METHODS
411         //
412
413         @Override
414         public synchronized String getFingerprint() {
415                 Hasher hash = Hashing.sha256().newHasher();
416                 hash.putString(profile.getFingerprint());
417
418                 hash.putString("Posts(");
419                 for (Post post : getPosts()) {
420                         hash.putString("Post(").putString(post.getId()).putString(")");
421                 }
422                 hash.putString(")");
423
424                 List<PostReply> replies = new ArrayList<PostReply>(getReplies());
425                 Collections.sort(replies, Reply.TIME_COMPARATOR);
426                 hash.putString("Replies(");
427                 for (PostReply reply : replies) {
428                         hash.putString("Reply(").putString(reply.getId()).putString(")");
429                 }
430                 hash.putString(")");
431
432                 List<String> likedPostIds = new ArrayList<String>(getLikedPostIds());
433                 Collections.sort(likedPostIds);
434                 hash.putString("LikedPosts(");
435                 for (String likedPostId : likedPostIds) {
436                         hash.putString("Post(").putString(likedPostId).putString(")");
437                 }
438                 hash.putString(")");
439
440                 List<String> likedReplyIds = new ArrayList<String>(getLikedReplyIds());
441                 Collections.sort(likedReplyIds);
442                 hash.putString("LikedReplies(");
443                 for (String likedReplyId : likedReplyIds) {
444                         hash.putString("Reply(").putString(likedReplyId).putString(")");
445                 }
446                 hash.putString(")");
447
448                 hash.putString("Albums(");
449                 for (Album album : rootAlbum.getAlbums()) {
450                         if (!Album.NOT_EMPTY.apply(album)) {
451                                 continue;
452                         }
453                         hash.putString(album.getFingerprint());
454                 }
455                 hash.putString(")");
456
457                 return hash.hash().toString();
458         }
459
460         //
461         // INTERFACE Comparable<Sone>
462         //
463
464         @Override
465         public int compareTo(Sone sone) {
466                 return NICE_NAME_COMPARATOR.compare(this, sone);
467         }
468
469         //
470         // OBJECT METHODS
471         //
472
473         @Override
474         public int hashCode() {
475                 return id.hashCode();
476         }
477
478         @Override
479         public boolean equals(Object object) {
480                 if (!(object instanceof Sone)) {
481                         return false;
482                 }
483                 return ((Sone) object).getId().equals(id);
484         }
485
486         @Override
487         public String toString() {
488                 return getClass().getName() + "[id=" + id + ",requestUri=" + requestUri + ",insertUri(" + String.valueOf(insertUri).length() + "),friends(" + friendSones.size() + "),posts(" + posts.size() + "),replies(" + replies.size() + "),albums(" + getRootAlbum().getAlbums().size() + ")]";
489         }
490
491 }