3abd1d4143a475283c22135f8c27fb01f23957e6
[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 volatile 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) {
126                 this.database = database;
127                 this.id = id;
128                 this.local = local;
129                 rootAlbum = new DefaultAlbumBuilder(database, this, null).build();
130         }
131
132         //
133         // ACCESSORS
134         //
135
136         public String getId() {
137                 return id;
138         }
139
140         public Identity getIdentity() {
141                 return database.getIdentity(id).get();
142         }
143
144         public String getName() {
145                 return getIdentity().getNickname();
146         }
147
148         public boolean isLocal() {
149                 return local;
150         }
151
152         public FreenetURI getRequestUri() {
153                 return (requestUri != null) ? requestUri.setSuggestedEdition(latestEdition) : null;
154         }
155
156         public Sone setRequestUri(FreenetURI requestUri) {
157                 if (this.requestUri == null) {
158                         this.requestUri = requestUri.setKeyType("USK").setDocName("Sone").setMetaString(new String[0]);
159                         return this;
160                 }
161                 if (!this.requestUri.equalsKeypair(requestUri)) {
162                         logger.log(Level.WARNING, String.format("Request URI %s tried to overwrite %s!", requestUri, this.requestUri));
163                         return this;
164                 }
165                 return this;
166         }
167
168         public FreenetURI getInsertUri() {
169                 return (insertUri != null) ? insertUri.setSuggestedEdition(latestEdition) : null;
170         }
171
172         public Sone setInsertUri(FreenetURI insertUri) {
173                 if (this.insertUri == null) {
174                         this.insertUri = insertUri.setKeyType("USK").setDocName("Sone").setMetaString(new String[0]);
175                         return this;
176                 }
177                 if (!this.insertUri.equalsKeypair(insertUri)) {
178                         logger.log(Level.WARNING, String.format("Request URI %s tried to overwrite %s!", insertUri, this.insertUri));
179                         return this;
180                 }
181                 return this;
182         }
183
184         public long getLatestEdition() {
185                 return latestEdition;
186         }
187
188         public void setLatestEdition(long latestEdition) {
189                 if (!(latestEdition > this.latestEdition)) {
190                         logger.log(Level.FINE, String.format("New latest edition %d is not greater than current latest edition %d!", latestEdition, this.latestEdition));
191                         return;
192                 }
193                 this.latestEdition = latestEdition;
194         }
195
196         public long getTime() {
197                 return time;
198         }
199
200         public Sone setTime(long time) {
201                 this.time = time;
202                 return this;
203         }
204
205         public SoneStatus getStatus() {
206                 return status;
207         }
208
209         public Sone setStatus(SoneStatus status) {
210                 this.status = checkNotNull(status, "status must not be null");
211                 return this;
212         }
213
214         public Profile getProfile() {
215                 return new Profile(profile);
216         }
217
218         public void setProfile(Profile profile) {
219                 this.profile = new Profile(profile);
220         }
221
222         public Client getClient() {
223                 return client;
224         }
225
226         public Sone setClient(Client client) {
227                 this.client = client;
228                 return this;
229         }
230
231         public boolean isKnown() {
232                 return known;
233         }
234
235         public Sone setKnown(boolean known) {
236                 this.known = known;
237                 return this;
238         }
239
240         public List<String> getFriends() {
241                 List<String> friends = new ArrayList<String>(friendSones);
242                 return friends;
243         }
244
245         public boolean hasFriend(String friendSoneId) {
246                 return friendSones.contains(friendSoneId);
247         }
248
249         public Sone addFriend(String friendSone) {
250                 if (!friendSone.equals(id)) {
251                         friendSones.add(friendSone);
252                 }
253                 return this;
254         }
255
256         public Sone removeFriend(String friendSoneId) {
257                 friendSones.remove(friendSoneId);
258                 return this;
259         }
260
261         public List<Post> getPosts() {
262                 List<Post> sortedPosts;
263                 synchronized (this) {
264                         sortedPosts = new ArrayList<Post>(posts);
265                 }
266                 Collections.sort(sortedPosts, Post.TIME_COMPARATOR);
267                 return sortedPosts;
268         }
269
270         public Sone setPosts(Collection<Post> posts) {
271                 synchronized (this) {
272                         this.posts.clear();
273                         this.posts.addAll(posts);
274                 }
275                 return this;
276         }
277
278         public void addPost(Post post) {
279                 if (post.getSone().equals(this) && posts.add(post)) {
280                         logger.log(Level.FINEST, String.format("Adding %s to “%s”.", post, getName()));
281                 }
282         }
283
284         public void removePost(Post post) {
285                 if (post.getSone().equals(this)) {
286                         posts.remove(post);
287                 }
288         }
289
290         public Set<PostReply> getReplies() {
291                 return Collections.unmodifiableSet(replies);
292         }
293
294         public Sone setReplies(Collection<PostReply> replies) {
295                 this.replies.clear();
296                 this.replies.addAll(replies);
297                 return this;
298         }
299
300         public void addReply(PostReply reply) {
301                 if (reply.getSone().equals(this)) {
302                         replies.add(reply);
303                 }
304         }
305
306         public void removeReply(PostReply reply) {
307                 if (reply.getSone().equals(this)) {
308                         replies.remove(reply);
309                 }
310         }
311
312         public Set<String> getLikedPostIds() {
313                 return Collections.unmodifiableSet(likedPostIds);
314         }
315
316         public Sone setLikePostIds(Set<String> likedPostIds) {
317                 this.likedPostIds.clear();
318                 this.likedPostIds.addAll(likedPostIds);
319                 return this;
320         }
321
322         public boolean isLikedPostId(String postId) {
323                 return likedPostIds.contains(postId);
324         }
325
326         public Sone addLikedPostId(String postId) {
327                 likedPostIds.add(postId);
328                 return this;
329         }
330
331         public Sone removeLikedPostId(String postId) {
332                 likedPostIds.remove(postId);
333                 return this;
334         }
335
336         public Set<String> getLikedReplyIds() {
337                 return Collections.unmodifiableSet(likedReplyIds);
338         }
339
340         public Sone setLikeReplyIds(Set<String> likedReplyIds) {
341                 this.likedReplyIds.clear();
342                 this.likedReplyIds.addAll(likedReplyIds);
343                 return this;
344         }
345
346         public boolean isLikedReplyId(String replyId) {
347                 return likedReplyIds.contains(replyId);
348         }
349
350         public Sone addLikedReplyId(String replyId) {
351                 likedReplyIds.add(replyId);
352                 return this;
353         }
354
355         public Sone removeLikedReplyId(String replyId) {
356                 likedReplyIds.remove(replyId);
357                 return this;
358         }
359
360         public Album getRootAlbum() {
361                 return rootAlbum;
362         }
363
364         public Options getOptions() {
365                 return options;
366         }
367
368         /* TODO - remove this method again, maybe add an option provider */
369         public void setOptions(Options options) {
370                 this.options = options;
371         }
372
373         @Override
374         public AlbumBuilder newAlbumBuilder() {
375                 return new DefaultAlbumBuilder(database, this, rootAlbum.getId());
376         }
377
378         public PostBuilder newPostBuilder() {
379                 return new DefaultPostBuilder(database, getId()) {
380                         @Override
381                         public Post build(Optional<PostCreated> postCreated) {
382                                 Post post = super.build(postCreated);
383                                 database.storePost(post);
384                                 return post;
385                         }
386                 };
387         }
388
389         @Override
390         public PostReplyBuilder newPostReplyBuilder(String postId) throws IllegalStateException {
391                 return new DefaultPostReplyBuilder(database, getId(), postId) {
392                         @Override
393                         public PostReply build(Optional<PostReplyCreated> postReplyCreated) {
394                                 PostReply postReply = super.build(postReplyCreated);
395                                 database.storePostReply(postReply);
396                                 return postReply;
397                         }
398                 };
399         }
400
401         public Modifier modify() {
402                 return new Modifier() {
403                         private long latestEdition = DefaultSone.this.latestEdition;
404                         @Override
405                         public Modifier setLatestEdition(long latestEdition) {
406                                 this.latestEdition = latestEdition;
407                                 return this;
408                         }
409
410                         @Override
411                         public Sone update() {
412                                 DefaultSone.this.latestEdition = latestEdition;
413                                 return DefaultSone.this;
414                         }
415                 };
416         }
417
418         //
419         // FINGERPRINTABLE METHODS
420         //
421
422         @Override
423         public synchronized String getFingerprint() {
424                 Hasher hash = Hashing.sha256().newHasher();
425                 hash.putString(profile.getFingerprint());
426
427                 hash.putString("Posts(");
428                 for (Post post : getPosts()) {
429                         hash.putString("Post(").putString(post.getId()).putString(")");
430                 }
431                 hash.putString(")");
432
433                 List<PostReply> replies = new ArrayList<PostReply>(getReplies());
434                 Collections.sort(replies, Reply.TIME_COMPARATOR);
435                 hash.putString("Replies(");
436                 for (PostReply reply : replies) {
437                         hash.putString("Reply(").putString(reply.getId()).putString(")");
438                 }
439                 hash.putString(")");
440
441                 List<String> likedPostIds = new ArrayList<String>(getLikedPostIds());
442                 Collections.sort(likedPostIds);
443                 hash.putString("LikedPosts(");
444                 for (String likedPostId : likedPostIds) {
445                         hash.putString("Post(").putString(likedPostId).putString(")");
446                 }
447                 hash.putString(")");
448
449                 List<String> likedReplyIds = new ArrayList<String>(getLikedReplyIds());
450                 Collections.sort(likedReplyIds);
451                 hash.putString("LikedReplies(");
452                 for (String likedReplyId : likedReplyIds) {
453                         hash.putString("Reply(").putString(likedReplyId).putString(")");
454                 }
455                 hash.putString(")");
456
457                 hash.putString("Albums(");
458                 for (Album album : rootAlbum.getAlbums()) {
459                         if (!Album.NOT_EMPTY.apply(album)) {
460                                 continue;
461                         }
462                         hash.putString(album.getFingerprint());
463                 }
464                 hash.putString(")");
465
466                 return hash.hash().toString();
467         }
468
469         //
470         // INTERFACE Comparable<Sone>
471         //
472
473         @Override
474         public int compareTo(Sone sone) {
475                 return NICE_NAME_COMPARATOR.compare(this, sone);
476         }
477
478         //
479         // OBJECT METHODS
480         //
481
482         @Override
483         public int hashCode() {
484                 return id.hashCode();
485         }
486
487         @Override
488         public boolean equals(Object object) {
489                 if (!(object instanceof Sone)) {
490                         return false;
491                 }
492                 return ((Sone) object).getId().equals(id);
493         }
494
495         @Override
496         public String toString() {
497                 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() + ")]";
498         }
499
500 }