Extract memory-based identity database into its own class.
[Sone.git] / src / main / java / net / pterodactylus / sone / database / memory / MemoryDatabase.java
1 /*
2  * Sone - MemoryDatabase.java - Copyright © 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.database.memory;
19
20 import static com.google.common.base.Optional.fromNullable;
21 import static com.google.common.base.Preconditions.checkNotNull;
22 import static com.google.common.base.Predicates.not;
23 import static com.google.common.collect.FluentIterable.from;
24 import static java.util.Collections.emptyList;
25 import static java.util.logging.Logger.getLogger;
26 import static net.pterodactylus.sone.data.Sone.LOCAL_SONE_FILTER;
27
28 import java.util.ArrayList;
29 import java.util.Collection;
30 import java.util.Collections;
31 import java.util.Comparator;
32 import java.util.HashMap;
33 import java.util.HashSet;
34 import java.util.List;
35 import java.util.Map;
36 import java.util.Set;
37 import java.util.concurrent.locks.ReadWriteLock;
38 import java.util.concurrent.locks.ReentrantReadWriteLock;
39 import java.util.logging.Level;
40 import java.util.logging.Logger;
41
42 import net.pterodactylus.sone.data.Album;
43 import net.pterodactylus.sone.data.Image;
44 import net.pterodactylus.sone.data.Post;
45 import net.pterodactylus.sone.data.PostReply;
46 import net.pterodactylus.sone.data.Sone;
47 import net.pterodactylus.sone.data.impl.DefaultSoneBuilder;
48 import net.pterodactylus.sone.database.Database;
49 import net.pterodactylus.sone.database.DatabaseException;
50 import net.pterodactylus.sone.database.SoneBuilder;
51 import net.pterodactylus.sone.freenet.wot.Identity;
52 import net.pterodactylus.util.config.Configuration;
53 import net.pterodactylus.util.config.ConfigurationException;
54
55 import com.google.common.base.Function;
56 import com.google.common.base.Optional;
57 import com.google.common.collect.ArrayListMultimap;
58 import com.google.common.collect.HashMultimap;
59 import com.google.common.collect.ListMultimap;
60 import com.google.common.collect.SetMultimap;
61 import com.google.common.collect.SortedSetMultimap;
62 import com.google.common.collect.TreeMultimap;
63 import com.google.common.util.concurrent.AbstractService;
64 import com.google.inject.Inject;
65
66 /**
67  * Memory-based {@link Database} implementation.
68  *
69  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
70  */
71 public class MemoryDatabase extends AbstractService implements Database {
72
73         private static final Logger logger = getLogger(MemoryDatabase.class.getName());
74
75         /** The lock. */
76         private final ReadWriteLock lock = new ReentrantReadWriteLock();
77
78         /** The configuration. */
79         private final Configuration configuration;
80
81         private final Map<String, Sone> sones = new HashMap<String, Sone>();
82         private final MemoryIdentityDatabase memoryIdentityDatabase;
83         private final MemoryPostDatabase memoryPostDatabase;
84
85         /** All post replies by their ID. */
86         private final Map<String, PostReply> allPostReplies = new HashMap<String, PostReply>();
87         private final SetMultimap<String, String> likedPostRepliesBySone = HashMultimap.create();
88         private final SetMultimap<String, String> postReplyLikingSones = HashMultimap.create();
89
90         /** Replies sorted by Sone. */
91         private final SortedSetMultimap<String, PostReply> sonePostReplies = TreeMultimap.create(new Comparator<String>() {
92
93                 @Override
94                 public int compare(String leftString, String rightString) {
95                         return leftString.compareTo(rightString);
96                 }
97         }, PostReply.TIME_COMPARATOR);
98
99         /** Replies by post. */
100         private final SortedSetMultimap<String, PostReply> postReplies = TreeMultimap.create(new Comparator<String>() {
101
102                 @Override
103                 public int compare(String leftString, String rightString) {
104                         return leftString.compareTo(rightString);
105                 }
106         }, PostReply.TIME_COMPARATOR);
107
108         /** Whether post replies are known. */
109         private final Set<String> knownPostReplies = new HashSet<String>();
110
111         private final Map<String, Album> allAlbums = new HashMap<String, Album>();
112         private final ListMultimap<String, String> albumChildren = ArrayListMultimap.create();
113         private final ListMultimap<String, String> albumImages = ArrayListMultimap.create();
114
115         private final Map<String, Image> allImages = new HashMap<String, Image>();
116
117         /**
118          * Creates a new memory database.
119          *
120          * @param configuration
121          *              The configuration for loading and saving elements
122          */
123         @Inject
124         public MemoryDatabase(Configuration configuration) {
125                 this.configuration = configuration;
126                 memoryPostDatabase = new MemoryPostDatabase(this, lock, configuration);
127                 memoryIdentityDatabase = new MemoryIdentityDatabase(lock);
128         }
129
130         //
131         // SERVICE METHODS
132         //
133
134         @Override
135         protected void doStart() {
136                 memoryPostDatabase.start();
137                 loadKnownPostReplies();
138                 notifyStarted();
139         }
140
141         @Override
142         protected void doStop() {
143                 try {
144                         memoryPostDatabase.stop();
145                 } catch (DatabaseException de1) {
146                         logger.log(Level.WARNING, "Could not stop post database!", de1);
147                 }
148                 notifyStopped();
149         }
150
151         @Override
152         public Optional<Identity> getIdentity(String identityId) {
153                 return memoryIdentityDatabase.getIdentity(identityId);
154         }
155
156         @Override
157         public void storeIdentity(Identity identitiy) {
158                 memoryIdentityDatabase.storeIdentity(identitiy);
159         }
160
161         @Override
162         public Function<String, Optional<Sone>> getSone() {
163                 return new Function<String, Optional<Sone>>() {
164                         @Override
165                         public Optional<Sone> apply(String soneId) {
166                                 return (soneId == null) ? Optional.<Sone>absent() : getSone(soneId);
167                         }
168                 };
169         }
170
171         @Override
172         public Optional<Sone> getSone(String soneId) {
173                 lock.readLock().lock();
174                 try {
175                         return fromNullable(sones.get(soneId));
176                 } finally {
177                         lock.readLock().unlock();
178                 }
179         }
180
181         @Override
182         public Collection<Sone> getSones() {
183                 lock.readLock().lock();
184                 try {
185                         return Collections.unmodifiableCollection(sones.values());
186                 } finally {
187                         lock.readLock().unlock();
188                 }
189         }
190
191         @Override
192         public Collection<Sone> getLocalSones() {
193                 lock.readLock().lock();
194                 try {
195                         return from(getSones()).filter(LOCAL_SONE_FILTER).toSet();
196                 } finally {
197                         lock.readLock().unlock();
198                 }
199         }
200
201         @Override
202         public Collection<Sone> getRemoteSones() {
203                 lock.readLock().lock();
204                 try {
205                         return from(getSones()).filter(not(LOCAL_SONE_FILTER)).toSet();
206                 } finally {
207                         lock.readLock().unlock();
208                 }
209         }
210
211         @Override
212         public void storeSone(Sone sone) {
213                 lock.writeLock().lock();
214                 try {
215                         sones.put(sone.getId(), sone);
216                 } finally {
217                         lock.writeLock().unlock();
218                 }
219         }
220
221         @Override
222         public SoneBuilder newSoneBuilder() {
223                 return new DefaultSoneBuilder(this) {
224                         @Override
225                         public Sone build(Optional<SoneCreated> soneCreated) throws IllegalStateException {
226                                 Sone sone = super.build(soneCreated);
227                                 lock.writeLock().lock();
228                                 try {
229                                         sones.put(sone.getId(), sone);
230                                 } finally {
231                                         lock.writeLock().unlock();
232                                 }
233                                 return sone;
234                         }
235                 };
236         }
237
238         //
239         // POSTPROVIDER METHODS
240         //
241
242         @Override
243         public Function<String, Optional<Post>> getPost() {
244                 return memoryPostDatabase.getPost();
245         }
246
247         @Override
248         public Optional<Post> getPost(String postId) {
249                 return memoryPostDatabase.getPost(postId);
250         }
251
252         @Override
253         public Collection<Post> getPosts(String soneId) {
254                 return memoryPostDatabase.getPosts(soneId);
255         }
256
257         @Override
258         public Collection<Post> getDirectedPosts(String recipientId) {
259                 return memoryPostDatabase.getDirectedPosts(recipientId);
260         }
261
262         /**
263          * Returns whether the given post is known.
264          *
265          * @param post
266          *              The post
267          * @return {@code true} if the post is known, {@code false} otherwise
268          */
269         @Override
270         public boolean isPostKnown(Post post) {
271                 return memoryPostDatabase.isPostKnown(post);
272         }
273
274         /**
275          * Sets whether the given post is known.
276          *
277          * @param post
278          *              The post
279          */
280         @Override
281         public void setPostKnown(Post post) {
282                 memoryPostDatabase.setPostKnown(post);
283         }
284
285         @Override
286         public void likePost(Post post, Sone localSone) {
287                 memoryPostDatabase.likePost(post, localSone);
288         }
289
290         @Override
291         public void unlikePost(Post post, Sone localSone) {
292                 memoryPostDatabase.unlikePost(post, localSone);
293         }
294
295         public boolean isLiked(Post post, Sone sone) {
296                 return memoryPostDatabase.isLiked(post, sone);
297         }
298
299         @Override
300         public Set<Sone> getLikes(Post post) {
301                 return memoryPostDatabase.getLikes(post);
302         }
303
304         //
305         // POSTSTORE METHODS
306         //
307
308         @Override
309         public void storePost(Post post) {
310                 memoryPostDatabase.storePost(post);
311         }
312
313         @Override
314         public void removePost(Post post) {
315                 memoryPostDatabase.removePost(post);
316         }
317
318         @Override
319         public void storePosts(Sone sone, Collection<Post> posts) throws IllegalArgumentException {
320                 /* verify that all posts are from the same Sone. */
321
322                 memoryPostDatabase.storePosts(sone, posts);
323         }
324
325         @Override
326         public void removePosts(Sone sone) {
327                 memoryPostDatabase.removePosts(sone);
328         }
329
330         //
331         // POSTREPLYPROVIDER METHODS
332         //
333
334         @Override
335         public Optional<PostReply> getPostReply(String id) {
336                 lock.readLock().lock();
337                 try {
338                         return fromNullable(allPostReplies.get(id));
339                 } finally {
340                         lock.readLock().unlock();
341                 }
342         }
343
344         @Override
345         public List<PostReply> getReplies(String postId) {
346                 lock.readLock().lock();
347                 try {
348                         if (!postReplies.containsKey(postId)) {
349                                 return emptyList();
350                         }
351                         return new ArrayList<PostReply>(postReplies.get(postId));
352                 } finally {
353                         lock.readLock().unlock();
354                 }
355         }
356
357         @Override
358         public void likePostReply(PostReply postReply, Sone localSone) {
359                 lock.writeLock().lock();
360                 try {
361                         likedPostRepliesBySone.put(localSone.getId(), postReply.getId());
362                         postReplyLikingSones.put(postReply.getId(), localSone.getId());
363                 } finally {
364                         lock.writeLock().unlock();
365                 }
366         }
367
368         @Override
369         public void unlikePostReply(PostReply postReply, Sone localSone) {
370                 lock.writeLock().lock();
371                 try {
372                         likedPostRepliesBySone.remove(localSone.getId(), postReply.getId());
373                         postReplyLikingSones.remove(postReply.getId(), localSone.getId());
374                 } finally {
375                         lock.writeLock().unlock();
376                 }
377         }
378
379         @Override
380         public boolean isLiked(PostReply postReply, Sone sone) {
381                 lock.readLock().lock();
382                 try {
383                         return postReplyLikingSones.containsEntry(postReply.getId(), sone.getId());
384                 } finally {
385                         lock.readLock().unlock();
386                 }
387         }
388
389         @Override
390         public Set<Sone> getLikes(PostReply postReply) {
391                 lock.readLock().lock();
392                 try {
393                         return from(postReplyLikingSones.get(postReply.getId())).transform(getSone()).transformAndConcat(this.<Sone>unwrap()).toSet();
394                 } finally {
395                         lock.readLock().unlock();
396                 }
397         }
398
399         //
400         // POSTREPLYSTORE METHODS
401         //
402
403         /**
404          * Returns whether the given post reply is known.
405          *
406          * @param postReply
407          *              The post reply
408          * @return {@code true} if the given post reply is known, {@code false}
409          *         otherwise
410          */
411         public boolean isPostReplyKnown(PostReply postReply) {
412                 lock.readLock().lock();
413                 try {
414                         return knownPostReplies.contains(postReply.getId());
415                 } finally {
416                         lock.readLock().unlock();
417                 }
418         }
419
420         @Override
421         public void setPostReplyKnown(PostReply postReply) {
422                 lock.writeLock().lock();
423                 try {
424                         knownPostReplies.add(postReply.getId());
425                 } finally {
426                         lock.writeLock().unlock();
427                 }
428         }
429
430         @Override
431         public void storePostReply(PostReply postReply) {
432                 lock.writeLock().lock();
433                 try {
434                         allPostReplies.put(postReply.getId(), postReply);
435                         postReplies.put(postReply.getPostId(), postReply);
436                 } finally {
437                         lock.writeLock().unlock();
438                 }
439         }
440
441         @Override
442         public void storePostReplies(Sone sone, Collection<PostReply> postReplies) {
443                 checkNotNull(sone, "sone must not be null");
444                 /* verify that all posts are from the same Sone. */
445                 for (PostReply postReply : postReplies) {
446                         if (!sone.equals(postReply.getSone())) {
447                                 throw new IllegalArgumentException(String.format("PostReply from different Sone found: %s", postReply));
448                         }
449                 }
450
451                 lock.writeLock().lock();
452                 try {
453                         /* remove all post replies of the Sone. */
454                         for (PostReply postReply : getRepliesFrom(sone.getId())) {
455                                 removePostReply(postReply);
456                         }
457                         for (PostReply postReply : postReplies) {
458                                 allPostReplies.put(postReply.getId(), postReply);
459                                 sonePostReplies.put(postReply.getSone().getId(), postReply);
460                                 this.postReplies.put(postReply.getPostId(), postReply);
461                         }
462                 } finally {
463                         lock.writeLock().unlock();
464                 }
465         }
466
467         @Override
468         public void removePostReply(PostReply postReply) {
469                 lock.writeLock().lock();
470                 try {
471                         allPostReplies.remove(postReply.getId());
472                         postReplies.remove(postReply.getPostId(), postReply);
473                 } finally {
474                         lock.writeLock().unlock();
475                 }
476         }
477
478         @Override
479         public void removePostReplies(Sone sone) {
480                 checkNotNull(sone, "sone must not be null");
481
482                 lock.writeLock().lock();
483                 try {
484                         for (PostReply postReply : sone.getReplies()) {
485                                 removePostReply(postReply);
486                         }
487                 } finally {
488                         lock.writeLock().unlock();
489                 }
490         }
491
492         //
493         // ALBUMPROVDER METHODS
494         //
495
496         @Override
497         public Optional<Album> getAlbum(String albumId) {
498                 lock.readLock().lock();
499                 try {
500                         return fromNullable(allAlbums.get(albumId));
501                 } finally {
502                         lock.readLock().unlock();
503                 }
504         }
505
506         @Override
507         public List<Album> getAlbums(Album parent) {
508                 lock.readLock().lock();
509                 try {
510                         return from(albumChildren.get(parent.getId())).transformAndConcat(getAlbum()).toList();
511                 } finally {
512                         lock.readLock().unlock();
513                 }
514         }
515
516         @Override
517         public void moveUp(Album album) {
518                 lock.writeLock().lock();
519                 try {
520                         List<String> albums = albumChildren.get(album.getParent().getId());
521                         int currentIndex = albums.indexOf(album.getId());
522                         if (currentIndex == 0) {
523                                 return;
524                         }
525                         albums.remove(album.getId());
526                         albums.add(currentIndex - 1, album.getId());
527                 } finally {
528                         lock.writeLock().unlock();
529                 }
530         }
531
532         @Override
533         public void moveDown(Album album) {
534                 lock.writeLock().lock();
535                 try {
536                         List<String> albums = albumChildren.get(album.getParent().getId());
537                         int currentIndex = albums.indexOf(album.getId());
538                         if (currentIndex == (albums.size() - 1)) {
539                                 return;
540                         }
541                         albums.remove(album.getId());
542                         albums.add(currentIndex + 1, album.getId());
543                 } finally {
544                         lock.writeLock().unlock();
545                 }
546         }
547
548         //
549         // ALBUMSTORE METHODS
550         //
551
552         @Override
553         public void storeAlbum(Album album) {
554                 lock.writeLock().lock();
555                 try {
556                         allAlbums.put(album.getId(), album);
557                         if (!album.isRoot()) {
558                                 albumChildren.put(album.getParent().getId(), album.getId());
559                         }
560                 } finally {
561                         lock.writeLock().unlock();
562                 }
563         }
564
565         @Override
566         public void removeAlbum(Album album) {
567                 lock.writeLock().lock();
568                 try {
569                         allAlbums.remove(album.getId());
570                         albumChildren.remove(album.getParent().getId(), album.getId());
571                 } finally {
572                         lock.writeLock().unlock();
573                 }
574         }
575
576         //
577         // IMAGEPROVIDER METHODS
578         //
579
580         @Override
581         public Optional<Image> getImage(String imageId) {
582                 lock.readLock().lock();
583                 try {
584                         return fromNullable(allImages.get(imageId));
585                 } finally {
586                         lock.readLock().unlock();
587                 }
588         }
589
590         @Override
591         public List<Image> getImages(Album parent) {
592                 lock.readLock().lock();
593                 try {
594                         return from(albumImages.get(parent.getId())).transformAndConcat(getImage()).toList();
595                 } finally {
596                         lock.readLock().unlock();
597                 }
598         }
599
600         @Override
601         public void moveUp(Image image) {
602                 lock.writeLock().lock();
603                 try {
604                         List<String> images = albumImages.get(image.getAlbum().getId());
605                         int currentIndex = images.indexOf(image.getId());
606                         if (currentIndex == 0) {
607                                 return;
608                         }
609                         images.remove(image.getId());
610                         images.add(currentIndex - 1, image.getId());
611                 } finally {
612                         lock.writeLock().unlock();
613                 }
614         }
615
616         @Override
617         public void moveDown(Image image) {
618                 lock.writeLock().lock();
619                 try {
620                         List<String> images = albumChildren.get(image.getAlbum().getId());
621                         int currentIndex = images.indexOf(image.getId());
622                         if (currentIndex == (images.size() - 1)) {
623                                 return;
624                         }
625                         images.remove(image.getId());
626                         images.add(currentIndex + 1, image.getId());
627                 } finally {
628                         lock.writeLock().unlock();
629                 }
630         }
631
632         //
633         // IMAGESTORE METHODS
634         //
635
636         @Override
637         public void storeImage(Image image) {
638                 lock.writeLock().lock();
639                 try {
640                         allImages.put(image.getId(), image);
641                         albumImages.put(image.getAlbum().getId(), image.getId());
642                 } finally {
643                         lock.writeLock().unlock();
644                 }
645         }
646
647         @Override
648         public void removeImage(Image image) {
649                 lock.writeLock().lock();
650                 try {
651                         allImages.remove(image.getId());
652                         albumImages.remove(image.getAlbum().getId(), image.getId());
653                 } finally {
654                         lock.writeLock().unlock();
655                 }
656         }
657
658         //
659         // PRIVATE METHODS
660         //
661
662         /**
663          * Returns all replies by the given Sone.
664          *
665          * @param id
666          *              The ID of the Sone
667          * @return The post replies of the Sone, sorted by time (newest first)
668          */
669         private Collection<PostReply> getRepliesFrom(String id) {
670                 lock.readLock().lock();
671                 try {
672                         if (sonePostReplies.containsKey(id)) {
673                                 return Collections.unmodifiableCollection(sonePostReplies.get(id));
674                         }
675                         return Collections.emptySet();
676                 } finally {
677                         lock.readLock().unlock();
678                 }
679         }
680
681         /** Loads the known post replies. */
682         private void loadKnownPostReplies() {
683                 lock.writeLock().lock();
684                 try {
685                         int replyCounter = 0;
686                         while (true) {
687                                 String knownReplyId = configuration.getStringValue("KnownReplies/" + replyCounter++ + "/ID").getValue(null);
688                                 if (knownReplyId == null) {
689                                         break;
690                                 }
691                                 knownPostReplies.add(knownReplyId);
692                         }
693                 } finally {
694                         lock.writeLock().unlock();
695                 }
696         }
697
698         /**
699          * Saves the known post replies to the configuration.
700          *
701          * @throws DatabaseException
702          *              if a configuration error occurs
703          */
704         private void saveKnownPostReplies() throws DatabaseException {
705                 lock.readLock().lock();
706                 try {
707                         int replyCounter = 0;
708                         for (String knownReplyId : knownPostReplies) {
709                                 configuration.getStringValue("KnownReplies/" + replyCounter++ + "/ID").setValue(knownReplyId);
710                         }
711                         configuration.getStringValue("KnownReplies/" + replyCounter + "/ID").setValue(null);
712                 } catch (ConfigurationException ce1) {
713                         throw new DatabaseException("Could not save database.", ce1);
714                 } finally {
715                         lock.readLock().unlock();
716                 }
717         }
718
719         private Function<String, Iterable<Album>> getAlbum() {
720                 return new Function<String, Iterable<Album>>() {
721                         @Override
722                         public Iterable<Album> apply(String input) {
723                                 return (input == null) ? Collections.<Album>emptyList() : getAlbum(input).asSet();
724                         }
725                 };
726         }
727
728         private Function<String, Iterable<Image>> getImage() {
729                 return new Function<String, Iterable<Image>>() {
730                         @Override
731                         public Iterable<Image> apply(String input) {
732                                 return (input == null) ? Collections.<Image>emptyList() : getImage(input).asSet();
733                         }
734                 };
735         }
736
737         static <T> Function<Optional<T>, Iterable<T>> unwrap() {
738                 return new Function<Optional<T>, Iterable<T>>() {
739                         @Override
740                         public Iterable<T> apply(Optional<T> input) {
741                                 return (input == null) ? Collections.<T>emptyList() : input.asSet();
742                         }
743                 };
744         }
745
746 }