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