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