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