a197c465f9b33014458c1861a2f62387931041a2
[Sone.git] / src / main / java / net / pterodactylus / sone / data / impl / SoneImpl.java
1 /*
2  * Sone - SoneImpl.java - Copyright © 2010–2020 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 import static java.lang.String.format;
22 import static java.nio.charset.StandardCharsets.UTF_8;
23 import static java.util.logging.Logger.getLogger;
24 import static net.pterodactylus.sone.data.PostKt.newestPostFirst;
25 import static net.pterodactylus.sone.data.ReplyKt.newestReplyFirst;
26 import static net.pterodactylus.sone.data.SoneKt.*;
27
28 import java.net.MalformedURLException;
29 import java.util.ArrayList;
30 import java.util.Collection;
31 import java.util.Collections;
32 import java.util.List;
33 import java.util.Set;
34 import java.util.concurrent.CopyOnWriteArraySet;
35 import java.util.logging.Level;
36 import java.util.logging.Logger;
37
38 import javax.annotation.Nonnull;
39 import javax.annotation.Nullable;
40
41 import net.pterodactylus.sone.data.Album;
42 import net.pterodactylus.sone.data.AlbumKt;
43 import net.pterodactylus.sone.data.Client;
44 import net.pterodactylus.sone.data.Post;
45 import net.pterodactylus.sone.data.PostReply;
46 import net.pterodactylus.sone.data.Profile;
47 import net.pterodactylus.sone.data.Reply;
48 import net.pterodactylus.sone.data.Sone;
49 import net.pterodactylus.sone.data.SoneOptions;
50 import net.pterodactylus.sone.data.SoneOptions.DefaultSoneOptions;
51 import net.pterodactylus.sone.database.Database;
52 import net.pterodactylus.sone.freenet.wot.Identity;
53
54 import freenet.keys.FreenetURI;
55
56 import com.google.common.hash.Hasher;
57 import com.google.common.hash.Hashing;
58
59 /**
60  * {@link Sone} implementation.
61  * <p/>
62  * Operations that modify the Sone need to synchronize on the Sone in question.
63  */
64 public class SoneImpl implements Sone {
65
66         /** The logger. */
67         private static final Logger logger = getLogger(SoneImpl.class.getName());
68
69         /** The database. */
70         private final Database database;
71
72         /** The ID of this Sone. */
73         private final String id;
74
75         /** Whether the Sone is local. */
76         private final boolean local;
77
78         /** The identity of this Sone. */
79         private final Identity identity;
80
81         /** The latest edition of the Sone. */
82         private volatile long latestEdition;
83
84         /** The time of the last inserted update. */
85         private volatile long time;
86
87         /** The status of this Sone. */
88         private volatile SoneStatus status = SoneStatus.unknown;
89
90         /** The profile of this Sone. */
91         private volatile Profile profile = new Profile(this);
92
93         /** The client used by the Sone. */
94         private volatile Client client;
95
96         /** Whether this Sone is known. */
97         private volatile boolean known;
98
99         /** All posts. */
100         private final Set<Post> posts = new CopyOnWriteArraySet<>();
101
102         /** All replies. */
103         private final Set<PostReply> replies = new CopyOnWriteArraySet<>();
104
105         /** The IDs of all liked posts. */
106         private final Set<String> likedPostIds = new CopyOnWriteArraySet<>();
107
108         /** The IDs of all liked replies. */
109         private final Set<String> likedReplyIds = new CopyOnWriteArraySet<>();
110
111         /** The root album containing all albums. */
112         private final Album rootAlbum = new AlbumImpl(this);
113
114         /** Sone-specific options. */
115         private SoneOptions options = new DefaultSoneOptions();
116
117         /**
118          * Creates a new Sone.
119          *
120          * @param database The database
121          * @param identity
122          *              The identity of the Sone
123          * @param local
124          *              {@code true} if the Sone is a local Sone, {@code false} otherwise
125          */
126         public SoneImpl(Database database, Identity identity, boolean local) {
127                 this.database = database;
128                 this.id = identity.getId();
129                 this.identity = identity;
130                 this.local = local;
131         }
132
133         //
134         // ACCESSORS
135         //
136
137         /**
138          * Returns the identity of this Sone.
139          *
140          * @return The identity of this Sone
141          */
142         @Nonnull
143         public String getId() {
144                 return id;
145         }
146
147         /**
148          * Returns the identity of this Sone.
149          *
150          * @return The identity of this Sone
151          */
152         @Nonnull
153         public Identity getIdentity() {
154                 return identity;
155         }
156
157         /**
158          * Returns the name of this Sone.
159          *
160          * @return The name of this Sone
161          */
162         @Nonnull
163         public String getName() {
164                 return (identity != null) ? identity.getNickname() : null;
165         }
166
167         /**
168          * Returns whether this Sone is a local Sone.
169          *
170          * @return {@code true} if this Sone is a local Sone, {@code false} otherwise
171          */
172         public boolean isLocal() {
173                 return local;
174         }
175
176         /**
177          * Returns the request URI of this Sone.
178          *
179          * @return The request URI of this Sone
180          */
181         @Nonnull
182         public FreenetURI getRequestUri() {
183                 try {
184                         return new FreenetURI(getIdentity().getRequestUri())
185                                         .setKeyType("USK")
186                                         .setDocName("Sone")
187                                         .setMetaString(new String[0])
188                                         .setSuggestedEdition(latestEdition);
189                 } catch (MalformedURLException e) {
190                         throw new IllegalStateException(
191                                         format("Identity %s's request URI is incorrect.",
192                                                         getIdentity()), e);
193                 }
194         }
195
196         /**
197          * Returns the latest edition of this Sone.
198          *
199          * @return The latest edition of this Sone
200          */
201         public long getLatestEdition() {
202                 return latestEdition;
203         }
204
205         /**
206          * Sets the latest edition of this Sone. If the given latest edition is not
207          * greater than the current latest edition, the latest edition of this Sone is
208          * not changed.
209          *
210          * @param latestEdition
211          *              The latest edition of this Sone
212          */
213         public void setLatestEdition(long latestEdition) {
214                 if (!(latestEdition > this.latestEdition)) {
215                         logger.log(Level.FINE, String.format("New latest edition %d is not greater than current latest edition %d!", latestEdition, this.latestEdition));
216                         return;
217                 }
218                 this.latestEdition = latestEdition;
219         }
220
221         /**
222          * Return the time of the last inserted update of this Sone.
223          *
224          * @return The time of the update (in milliseconds since Jan 1, 1970 UTC)
225          */
226         public long getTime() {
227                 return time;
228         }
229
230         /**
231          * Sets the time of the last inserted update of this Sone.
232          *
233          * @param time
234          *              The time of the update (in milliseconds since Jan 1, 1970 UTC)
235          * @return This Sone (for method chaining)
236          */
237         @Nonnull
238         public Sone setTime(long time) {
239                 this.time = time;
240                 return this;
241         }
242
243         /**
244          * Returns the status of this Sone.
245          *
246          * @return The status of this Sone
247          */
248         @Nonnull
249         public SoneStatus getStatus() {
250                 return status;
251         }
252
253         /**
254          * Sets the new status of this Sone.
255          *
256          * @param status
257          *              The new status of this Sone
258          * @return This Sone
259          * @throws IllegalArgumentException
260          *              if {@code status} is {@code null}
261          */
262         @Nonnull
263         public Sone setStatus(@Nonnull SoneStatus status) {
264                 this.status = checkNotNull(status, "status must not be null");
265                 return this;
266         }
267
268         /**
269          * Returns a copy of the profile. If you want to update values in the profile
270          * of this Sone, update the values in the returned {@link Profile} and use
271          * {@link #setProfile(Profile)} to change the profile in this Sone.
272          *
273          * @return A copy of the profile
274          */
275         @Nonnull
276         public Profile getProfile() {
277                 return new Profile(profile);
278         }
279
280         /**
281          * Sets the profile of this Sone. A copy of the given profile is stored so that
282          * subsequent modifications of the given profile are not reflected in this
283          * Sone!
284          *
285          * @param profile
286          *              The profile to set
287          */
288         public void setProfile(@Nonnull Profile profile) {
289                 this.profile = new Profile(profile);
290         }
291
292         /**
293          * Returns the client used by this Sone.
294          *
295          * @return The client used by this Sone, or {@code null}
296          */
297         @Nullable
298         public Client getClient() {
299                 return client;
300         }
301
302         /**
303          * Sets the client used by this Sone.
304          *
305          * @param client
306          *              The client used by this Sone, or {@code null}
307          * @return This Sone (for method chaining)
308          */
309         @Nonnull
310         public Sone setClient(@Nullable Client client) {
311                 this.client = client;
312                 return this;
313         }
314
315         /**
316          * Returns whether this Sone is known.
317          *
318          * @return {@code true} if this Sone is known, {@code false} otherwise
319          */
320         public boolean isKnown() {
321                 return known;
322         }
323
324         /**
325          * Sets whether this Sone is known.
326          *
327          * @param known
328          *              {@code true} if this Sone is known, {@code false} otherwise
329          * @return This Sone
330          */
331         @Nonnull
332         public Sone setKnown(boolean known) {
333                 this.known = known;
334                 return this;
335         }
336
337         /**
338          * Returns all friend Sones of this Sone.
339          *
340          * @return The friend Sones of this Sone
341          */
342         @Nonnull
343         public Collection<String> getFriends() {
344                 return database.getFriends(this);
345         }
346
347         /**
348          * Returns whether this Sone has the given Sone as a friend Sone.
349          *
350          * @param friendSoneId
351          *              The ID of the Sone to check for
352          * @return {@code true} if this Sone has the given Sone as a friend, {@code
353          *         false} otherwise
354          */
355         public boolean hasFriend(@Nonnull String friendSoneId) {
356                 return database.isFriend(this, friendSoneId);
357         }
358
359         /**
360          * Returns the list of posts of this Sone, sorted by time, newest first.
361          *
362          * @return All posts of this Sone
363          */
364         @Nonnull
365         public List<Post> getPosts() {
366                 List<Post> sortedPosts;
367                 synchronized (this) {
368                         sortedPosts = new ArrayList<>(posts);
369                 }
370                 sortedPosts.sort(newestPostFirst());
371                 return sortedPosts;
372         }
373
374         /**
375          * Sets all posts of this Sone at once.
376          *
377          * @param posts
378          *              The new (and only) posts of this Sone
379          * @return This Sone (for method chaining)
380          */
381         @Nonnull
382         public Sone setPosts(@Nonnull Collection<Post> posts) {
383                 synchronized (this) {
384                         this.posts.clear();
385                         this.posts.addAll(posts);
386                 }
387                 return this;
388         }
389
390         /**
391          * Adds the given post to this Sone. The post will not be added if its {@link
392          * Post#getSone() Sone} is not this Sone.
393          *
394          * @param post
395          *              The post to add
396          */
397         public void addPost(@Nonnull Post post) {
398                 if (post.getSone().equals(this) && posts.add(post)) {
399                         logger.log(Level.FINEST, String.format("Adding %s to “%s”.", post, getName()));
400                 }
401         }
402
403         /**
404          * Removes the given post from this Sone.
405          *
406          * @param post
407          *              The post to remove
408          */
409         public void removePost(@Nonnull Post post) {
410                 if (post.getSone().equals(this)) {
411                         posts.remove(post);
412                 }
413         }
414
415         /**
416          * Returns all replies this Sone made.
417          *
418          * @return All replies this Sone made
419          */
420         @Nonnull
421         public Set<PostReply> getReplies() {
422                 return Collections.unmodifiableSet(replies);
423         }
424
425         /**
426          * Sets all replies of this Sone at once.
427          *
428          * @param replies
429          *              The new (and only) replies of this Sone
430          * @return This Sone (for method chaining)
431          */
432         @Nonnull
433         public Sone setReplies(@Nonnull Collection<PostReply> replies) {
434                 this.replies.clear();
435                 this.replies.addAll(replies);
436                 return this;
437         }
438
439         /**
440          * Adds a reply to this Sone. If the given reply was not made by this Sone,
441          * nothing is added to this Sone.
442          *
443          * @param reply
444          *              The reply to add
445          */
446         public void addReply(@Nonnull PostReply reply) {
447                 if (reply.getSone().equals(this)) {
448                         replies.add(reply);
449                 }
450         }
451
452         /**
453          * Removes a reply from this Sone.
454          *
455          * @param reply
456          *              The reply to remove
457          */
458         public void removeReply(@Nonnull PostReply reply) {
459                 if (reply.getSone().equals(this)) {
460                         replies.remove(reply);
461                 }
462         }
463
464         /**
465          * Returns the IDs of all liked posts.
466          *
467          * @return All liked posts’ IDs
468          */
469         @Nonnull
470         public Set<String> getLikedPostIds() {
471                 return Collections.unmodifiableSet(likedPostIds);
472         }
473
474         /**
475          * Sets the IDs of all liked posts.
476          *
477          * @param likedPostIds
478          *              All liked posts’ IDs
479          * @return This Sone (for method chaining)
480          */
481         @Nonnull
482         public Sone setLikePostIds(@Nonnull Set<String> likedPostIds) {
483                 this.likedPostIds.clear();
484                 this.likedPostIds.addAll(likedPostIds);
485                 return this;
486         }
487
488         /**
489          * Checks whether the given post ID is liked by this Sone.
490          *
491          * @param postId
492          *              The ID of the post
493          * @return {@code true} if this Sone likes the given post, {@code false}
494          *         otherwise
495          */
496         public boolean isLikedPostId(@Nonnull String postId) {
497                 return likedPostIds.contains(postId);
498         }
499
500         /**
501          * Adds the given post ID to the list of posts this Sone likes.
502          *
503          * @param postId
504          *              The ID of the post
505          * @return This Sone (for method chaining)
506          */
507         @Nonnull
508         public Sone addLikedPostId(@Nonnull String postId) {
509                 likedPostIds.add(postId);
510                 return this;
511         }
512
513         /**
514          * Removes the given post ID from the list of posts this Sone likes.
515          *
516          * @param postId
517          *              The ID of the post
518          */
519         public void removeLikedPostId(@Nonnull String postId) {
520                 likedPostIds.remove(postId);
521         }
522
523         /**
524          * Returns the IDs of all liked replies.
525          *
526          * @return All liked replies’ IDs
527          */
528         @Nonnull
529         public Set<String> getLikedReplyIds() {
530                 return Collections.unmodifiableSet(likedReplyIds);
531         }
532
533         /**
534          * Sets the IDs of all liked replies.
535          *
536          * @param likedReplyIds
537          *              All liked replies’ IDs
538          * @return This Sone (for method chaining)
539          */
540         @Nonnull
541         public Sone setLikeReplyIds(@Nonnull Set<String> likedReplyIds) {
542                 this.likedReplyIds.clear();
543                 this.likedReplyIds.addAll(likedReplyIds);
544                 return this;
545         }
546
547         /**
548          * Checks whether the given reply ID is liked by this Sone.
549          *
550          * @param replyId
551          *              The ID of the reply
552          * @return {@code true} if this Sone likes the given reply, {@code false}
553          *         otherwise
554          */
555         public boolean isLikedReplyId(@Nonnull String replyId) {
556                 return likedReplyIds.contains(replyId);
557         }
558
559         /**
560          * Adds the given reply ID to 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         @Nonnull
567         public Sone addLikedReplyId(@Nonnull String replyId) {
568                 likedReplyIds.add(replyId);
569                 return this;
570         }
571
572         /**
573          * Removes the given post ID from the list of replies this Sone likes.
574          *
575          * @param replyId
576          *              The ID of the reply
577          */
578         public void removeLikedReplyId(@Nonnull String replyId) {
579                 likedReplyIds.remove(replyId);
580         }
581
582         /**
583          * Returns the root album that contains all visible albums of this Sone.
584          *
585          * @return The root album of this Sone
586          */
587         @Nonnull
588         public Album getRootAlbum() {
589                 return rootAlbum;
590         }
591
592         /**
593          * Returns Sone-specific options.
594          *
595          * @return The options of this Sone
596          */
597         @Nonnull
598         public SoneOptions getOptions() {
599                 return options;
600         }
601
602         /**
603          * Sets the options of this Sone.
604          *
605          * @param options
606          *              The options of this Sone
607          */
608         /* TODO - remove this method again, maybe add an option provider */
609         public void setOptions(@Nonnull SoneOptions options) {
610                 this.options = options;
611         }
612
613         //
614         // FINGERPRINTABLE METHODS
615         //
616
617         /** {@inheritDoc} */
618         @Override
619         public synchronized String getFingerprint() {
620                 Hasher hash = Hashing.sha256().newHasher();
621                 hash.putString(profile.getFingerprint(), UTF_8);
622
623                 hash.putString("Posts(", UTF_8);
624                 for (Post post : getPosts()) {
625                         hash.putString("Post(", UTF_8).putString(post.getId(), UTF_8).putString(")", UTF_8);
626                 }
627                 hash.putString(")", UTF_8);
628
629                 List<PostReply> replies = new ArrayList<>(getReplies());
630                 replies.sort(newestReplyFirst().reversed());
631                 hash.putString("Replies(", UTF_8);
632                 for (PostReply reply : replies) {
633                         hash.putString("Reply(", UTF_8).putString(reply.getId(), UTF_8).putString(")", UTF_8);
634                 }
635                 hash.putString(")", UTF_8);
636
637                 List<String> likedPostIds = new ArrayList<>(getLikedPostIds());
638                 Collections.sort(likedPostIds);
639                 hash.putString("LikedPosts(", UTF_8);
640                 for (String likedPostId : likedPostIds) {
641                         hash.putString("Post(", UTF_8).putString(likedPostId, UTF_8).putString(")", UTF_8);
642                 }
643                 hash.putString(")", UTF_8);
644
645                 List<String> likedReplyIds = new ArrayList<>(getLikedReplyIds());
646                 Collections.sort(likedReplyIds);
647                 hash.putString("LikedReplies(", UTF_8);
648                 for (String likedReplyId : likedReplyIds) {
649                         hash.putString("Reply(", UTF_8).putString(likedReplyId, UTF_8).putString(")", UTF_8);
650                 }
651                 hash.putString(")", UTF_8);
652
653                 hash.putString("Albums(", UTF_8);
654                 for (Album album : rootAlbum.getAlbums()) {
655                         if (!AlbumKt.notEmpty().invoke(album)) {
656                                 continue;
657                         }
658                         hash.putString(album.getFingerprint(), UTF_8);
659                 }
660                 hash.putString(")", UTF_8);
661
662                 return hash.hash().toString();
663         }
664
665         //
666         // INTERFACE Comparable<Sone>
667         //
668
669         /** {@inheritDoc} */
670         @Override
671         public int compareTo(Sone sone) {
672                 return niceNameComparator().compare(this, sone);
673         }
674
675         //
676         // OBJECT METHODS
677         //
678
679         /** {@inheritDoc} */
680         @Override
681         public int hashCode() {
682                 return id.hashCode();
683         }
684
685         /** {@inheritDoc} */
686         @Override
687         public boolean equals(Object object) {
688                 if (!(object instanceof Sone)) {
689                         return false;
690                 }
691                 return ((Sone) object).getId().equals(id);
692         }
693
694         /** {@inheritDoc} */
695         @Override
696         public String toString() {
697                 return getClass().getName() + "[identity=" + identity + ",posts(" + posts.size() + "),replies(" + replies.size() + "),albums(" + getRootAlbum().getAlbums().size() + ")]";
698         }
699
700 }