92d7d5fd735431b094f11eab8d03666754c9dd29
[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 addLikedPostId(String postId) {
323                 likedPostIds.add(postId);
324                 return this;
325         }
326
327         public Sone removeLikedPostId(String postId) {
328                 likedPostIds.remove(postId);
329                 return this;
330         }
331
332         public Set<String> getLikedReplyIds() {
333                 return Collections.unmodifiableSet(likedReplyIds);
334         }
335
336         public Sone setLikeReplyIds(Set<String> likedReplyIds) {
337                 this.likedReplyIds.clear();
338                 this.likedReplyIds.addAll(likedReplyIds);
339                 return this;
340         }
341
342         public boolean isLikedReplyId(String replyId) {
343                 return likedReplyIds.contains(replyId);
344         }
345
346         public Sone addLikedReplyId(String replyId) {
347                 likedReplyIds.add(replyId);
348                 return this;
349         }
350
351         public Sone removeLikedReplyId(String replyId) {
352                 likedReplyIds.remove(replyId);
353                 return this;
354         }
355
356         public Album getRootAlbum() {
357                 return rootAlbum;
358         }
359
360         public Options getOptions() {
361                 return options;
362         }
363
364         /* TODO - remove this method again, maybe add an option provider */
365         public void setOptions(Options options) {
366                 this.options = options;
367         }
368
369         @Override
370         public AlbumBuilder newAlbumBuilder() {
371                 return new DefaultAlbumBuilder(database, this, rootAlbum.getId());
372         }
373
374         public PostBuilder newPostBuilder() {
375                 return new DefaultPostBuilder(database, getId()) {
376                         @Override
377                         public Post build(Optional<PostCreated> postCreated) {
378                                 Post post = super.build(postCreated);
379                                 database.storePost(post);
380                                 return post;
381                         }
382                 };
383         }
384
385         @Override
386         public PostReplyBuilder newPostReplyBuilder(String postId) throws IllegalStateException {
387                 return new DefaultPostReplyBuilder(database, getId(), postId) {
388                         @Override
389                         public PostReply build(Optional<PostReplyCreated> postReplyCreated) {
390                                 PostReply postReply = super.build(postReplyCreated);
391                                 database.storePostReply(postReply);
392                                 return postReply;
393                         }
394                 };
395         }
396
397         public Modifier modify() {
398                 return new Modifier() {
399                         private long latestEdition = DefaultSone.this.latestEdition;
400                         @Override
401                         public Modifier setLatestEdition(long latestEdition) {
402                                 this.latestEdition = latestEdition;
403                                 return this;
404                         }
405
406                         @Override
407                         public Sone update() {
408                                 DefaultSone.this.latestEdition = latestEdition;
409                                 return DefaultSone.this;
410                         }
411                 };
412         }
413
414         //
415         // FINGERPRINTABLE METHODS
416         //
417
418         @Override
419         public synchronized String getFingerprint() {
420                 Hasher hash = Hashing.sha256().newHasher();
421                 hash.putString(profile.getFingerprint());
422
423                 hash.putString("Posts(");
424                 for (Post post : getPosts()) {
425                         hash.putString("Post(").putString(post.getId()).putString(")");
426                 }
427                 hash.putString(")");
428
429                 List<PostReply> replies = new ArrayList<PostReply>(getReplies());
430                 Collections.sort(replies, Reply.TIME_COMPARATOR);
431                 hash.putString("Replies(");
432                 for (PostReply reply : replies) {
433                         hash.putString("Reply(").putString(reply.getId()).putString(")");
434                 }
435                 hash.putString(")");
436
437                 List<String> likedPostIds = new ArrayList<String>(getLikedPostIds());
438                 Collections.sort(likedPostIds);
439                 hash.putString("LikedPosts(");
440                 for (String likedPostId : likedPostIds) {
441                         hash.putString("Post(").putString(likedPostId).putString(")");
442                 }
443                 hash.putString(")");
444
445                 List<String> likedReplyIds = new ArrayList<String>(getLikedReplyIds());
446                 Collections.sort(likedReplyIds);
447                 hash.putString("LikedReplies(");
448                 for (String likedReplyId : likedReplyIds) {
449                         hash.putString("Reply(").putString(likedReplyId).putString(")");
450                 }
451                 hash.putString(")");
452
453                 hash.putString("Albums(");
454                 for (Album album : rootAlbum.getAlbums()) {
455                         if (!Album.NOT_EMPTY.apply(album)) {
456                                 continue;
457                         }
458                         hash.putString(album.getFingerprint());
459                 }
460                 hash.putString(")");
461
462                 return hash.hash().toString();
463         }
464
465         //
466         // INTERFACE Comparable<Sone>
467         //
468
469         @Override
470         public int compareTo(Sone sone) {
471                 return NICE_NAME_COMPARATOR.compare(this, sone);
472         }
473
474         //
475         // OBJECT METHODS
476         //
477
478         @Override
479         public int hashCode() {
480                 return id.hashCode();
481         }
482
483         @Override
484         public boolean equals(Object object) {
485                 if (!(object instanceof Sone)) {
486                         return false;
487                 }
488                 return ((Sone) object).getId().equals(id);
489         }
490
491         @Override
492         public String toString() {
493                 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() + ")]";
494         }
495
496 }