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