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