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