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