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