83a886f361ed200897ca65bd020ca33ef285149a
[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.lang.String.format;
25 import static java.util.logging.Level.WARNING;
26 import static net.pterodactylus.sone.data.Reply.TIME_COMPARATOR;
27 import static net.pterodactylus.sone.data.Sone.LOCAL_SONE_FILTER;
28 import static net.pterodactylus.sone.data.Sone.toAllAlbums;
29 import static net.pterodactylus.sone.data.Sone.toAllImages;
30
31 import java.util.Collection;
32 import java.util.Collections;
33 import java.util.Comparator;
34 import java.util.HashMap;
35 import java.util.HashSet;
36 import java.util.List;
37 import java.util.Map;
38 import java.util.Set;
39 import java.util.concurrent.locks.ReadWriteLock;
40 import java.util.concurrent.locks.ReentrantReadWriteLock;
41 import java.util.logging.Level;
42 import java.util.logging.Logger;
43
44 import net.pterodactylus.sone.core.ConfigurationSoneParser;
45 import net.pterodactylus.sone.core.ConfigurationSoneParser.InvalidAlbumFound;
46 import net.pterodactylus.sone.core.ConfigurationSoneParser.InvalidImageFound;
47 import net.pterodactylus.sone.core.ConfigurationSoneParser.InvalidParentAlbumFound;
48 import net.pterodactylus.sone.core.ConfigurationSoneParser.InvalidPostFound;
49 import net.pterodactylus.sone.core.ConfigurationSoneParser.InvalidPostReplyFound;
50 import net.pterodactylus.sone.data.Album;
51 import net.pterodactylus.sone.data.Client;
52 import net.pterodactylus.sone.data.Image;
53 import net.pterodactylus.sone.data.Post;
54 import net.pterodactylus.sone.data.PostReply;
55 import net.pterodactylus.sone.data.Profile;
56 import net.pterodactylus.sone.data.Profile.Field;
57 import net.pterodactylus.sone.data.Sone;
58 import net.pterodactylus.sone.data.Sone.ShowCustomAvatars;
59 import net.pterodactylus.sone.data.impl.AlbumBuilderImpl;
60 import net.pterodactylus.sone.data.impl.ImageBuilderImpl;
61 import net.pterodactylus.sone.database.AlbumBuilder;
62 import net.pterodactylus.sone.database.Database;
63 import net.pterodactylus.sone.database.DatabaseException;
64 import net.pterodactylus.sone.database.ImageBuilder;
65 import net.pterodactylus.sone.database.PostBuilder;
66 import net.pterodactylus.sone.database.PostDatabase;
67 import net.pterodactylus.sone.database.PostReplyBuilder;
68 import net.pterodactylus.sone.database.SoneBuilder;
69 import net.pterodactylus.sone.database.SoneProvider;
70 import net.pterodactylus.sone.freenet.wot.OwnIdentity;
71 import net.pterodactylus.sone.main.SonePlugin;
72 import net.pterodactylus.sone.utils.Optionals;
73 import net.pterodactylus.util.config.Configuration;
74 import net.pterodactylus.util.config.ConfigurationException;
75
76 import com.google.common.base.Function;
77 import com.google.common.base.Optional;
78 import com.google.common.base.Predicate;
79 import com.google.common.collect.FluentIterable;
80 import com.google.common.collect.HashMultimap;
81 import com.google.common.collect.Multimap;
82 import com.google.common.collect.SortedSetMultimap;
83 import com.google.common.collect.TreeMultimap;
84 import com.google.common.primitives.Longs;
85 import com.google.common.util.concurrent.AbstractService;
86 import com.google.inject.Inject;
87 import com.google.inject.Singleton;
88
89 /**
90  * Memory-based {@link PostDatabase} implementation.
91  *
92  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
93  */
94 @Singleton
95 public class MemoryDatabase extends AbstractService implements Database {
96
97         private static final Logger logger = Logger.getLogger("Sone.Database.Memory");
98         private static final String LATEST_EDITION_PROPERTY = "Sone.LatestEdition";
99         /** The lock. */
100         private final ReadWriteLock lock = new ReentrantReadWriteLock();
101
102         /** The Sone provider. */
103         private final SoneProvider soneProvider;
104
105         /** The configuration. */
106         private final Configuration configuration;
107         private final ConfigurationLoader configurationLoader;
108
109         private final Set<String> localSones = new HashSet<String>();
110         private final Map<String, Sone> allSones = new HashMap<String, Sone>();
111         private final Map<String, String> lastInsertFingerprints = new HashMap<String, String>();
112
113         /** All posts by their ID. */
114         private final Map<String, Post> allPosts = new HashMap<String, Post>();
115
116         /** All posts by their Sones. */
117         private final Multimap<String, Post> sonePosts = HashMultimap.create();
118
119         /** Whether posts are known. */
120         private final Set<String> knownPosts = new HashSet<String>();
121
122         /** All post replies by their ID. */
123         private final Map<String, PostReply> allPostReplies = new HashMap<String, PostReply>();
124
125         /** Replies sorted by Sone. */
126         private final SortedSetMultimap<String, PostReply> sonePostReplies = TreeMultimap.create(new Comparator<String>() {
127
128                 @Override
129                 public int compare(String leftString, String rightString) {
130                         return leftString.compareTo(rightString);
131                 }
132         }, TIME_COMPARATOR);
133
134         /** Whether post replies are known. */
135         private final Set<String> knownPostReplies = new HashSet<String>();
136
137         private final Map<String, Album> allAlbums = new HashMap<String, Album>();
138         private final Multimap<String, Album> soneAlbums = HashMultimap.create();
139
140         private final Map<String, Image> allImages = new HashMap<String, Image>();
141         private final Multimap<String, Image> soneImages = HashMultimap.create();
142
143         private final MemoryBookmarkDatabase memoryBookmarkDatabase;
144         private final MemoryFriendDatabase memoryFriendDatabase;
145
146         /**
147          * Creates a new memory database.
148          *
149          * @param soneProvider
150          *              The Sone provider
151          * @param configuration
152          *              The configuration for loading and saving elements
153          */
154         @Inject
155         public MemoryDatabase(SoneProvider soneProvider, Configuration configuration) {
156                 this.soneProvider = soneProvider;
157                 this.configuration = configuration;
158                 this.configurationLoader = new ConfigurationLoader(configuration);
159                 memoryBookmarkDatabase =
160                                 new MemoryBookmarkDatabase(this, configurationLoader);
161                 memoryFriendDatabase = new MemoryFriendDatabase(configurationLoader);
162         }
163
164         //
165         // DATABASE METHODS
166         //
167
168
169         @Override
170         public Sone registerLocalSone(OwnIdentity ownIdentity) {
171                 final Sone localSone = loadLocalSone(ownIdentity);
172                 localSones.add(ownIdentity.getId());
173                 return localSone;
174         }
175
176         private Sone loadLocalSone(OwnIdentity ownIdentity) {
177                 Sone localSone = newSoneBuilder().local().from(ownIdentity).build();
178                 localSone.setLatestEdition(
179                                 Optional.fromNullable(
180                                                 Longs.tryParse(ownIdentity.getProperty(LATEST_EDITION_PROPERTY)))
181                                 .or(0L));
182                 localSone.setClient(new Client("Sone", SonePlugin.VERSION.toString()));
183                 localSone.setKnown(true);
184
185                 loadSone(localSone);
186                 return localSone;
187         }
188
189         public void loadSone(Sone sone) {
190                 long soneTime = configurationLoader.getLocalSoneTime(sone.getId());
191                 if (soneTime == -1) {
192                         return;
193                 }
194
195                 /* load profile. */
196                 ConfigurationSoneParser configurationSoneParser = new ConfigurationSoneParser(configuration, sone);
197                 Profile profile = configurationSoneParser.parseProfile();
198
199                 /* load posts. */
200                 Collection<Post> posts;
201                 try {
202                         posts = configurationSoneParser.parsePosts(this);
203                 } catch (InvalidPostFound ipf) {
204                         logger.log(Level.WARNING, "Invalid post found, aborting load!");
205                         return;
206                 }
207
208                 /* load replies. */
209                 Collection<PostReply> postReplies;
210                 try {
211                         postReplies = configurationSoneParser.parsePostReplies(this);
212                 } catch (InvalidPostReplyFound iprf) {
213                         logger.log(Level.WARNING, "Invalid reply found, aborting load!");
214                         return;
215                 }
216
217                 /* load post likes. */
218                 Set<String> likedPostIds = configurationSoneParser.parseLikedPostIds();
219
220                 /* load reply likes. */
221                 Set<String> likedReplyIds = configurationSoneParser.parseLikedPostReplyIds();
222
223                 /* load albums. */
224                 List<Album> topLevelAlbums;
225                 try {
226                         topLevelAlbums = configurationSoneParser.parseTopLevelAlbums(this);
227                 } catch (InvalidAlbumFound iaf) {
228                         logger.log(Level.WARNING, "Invalid album found, aborting load!");
229                         return;
230                 } catch (InvalidParentAlbumFound ipaf) {
231                         logger.log(Level.WARNING,
232                                         format("Invalid parent album ID: %s", ipaf.getAlbumParentId()));
233                         return;
234                 }
235
236                 /* load images. */
237                 try {
238                         configurationSoneParser.parseImages(this);
239                 } catch (InvalidImageFound iif) {
240                         logger.log(WARNING, "Invalid image found, aborting load!");
241                         return;
242                 } catch (InvalidParentAlbumFound ipaf) {
243                         logger.log(Level.WARNING,
244                                         format("Invalid album image (%s) encountered, aborting load!",
245                                                         ipaf.getAlbumParentId()));
246                         return;
247                 }
248
249                 /* load avatar. */
250                 String sonePrefix = "Sone/" + sone.getId();
251                 String avatarId = configuration.getStringValue(sonePrefix + "/Profile/Avatar").getValue(null);
252                 if (avatarId != null) {
253                         final Map<String, Image> images = configurationSoneParser.getImages();
254                         profile.setAvatar(images.get(avatarId));
255                 }
256
257                 /* load options. */
258                 sone.getOptions().setAutoFollow(configuration.getBooleanValue(sonePrefix + "/Options/AutoFollow").getValue(null));
259                 sone.getOptions().setSoneInsertNotificationEnabled(configuration.getBooleanValue(sonePrefix + "/Options/EnableSoneInsertNotifications").getValue(null));
260                 sone.getOptions().setShowNewSoneNotifications(configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewSones").getValue(null));
261                 sone.getOptions().setShowNewPostNotifications(configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewPosts").getValue(null));
262                 sone.getOptions().setShowNewReplyNotifications(configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewReplies").getValue(null));
263                 sone.getOptions().setShowCustomAvatars(ShowCustomAvatars.valueOf(
264                                 configuration.getStringValue(sonePrefix + "/Options/ShowCustomAvatars")
265                                                 .getValue(ShowCustomAvatars.NEVER.name())));
266
267                 /* if we’re still here, Sone was loaded successfully. */
268                 lock.writeLock().lock();
269                 try {
270                         sone.setTime(soneTime);
271                         sone.setProfile(profile);
272                         sone.setLikePostIds(likedPostIds);
273                         sone.setLikeReplyIds(likedReplyIds);
274
275                         String lastInsertFingerprint = configurationLoader.getLastInsertFingerprint(sone.getId());
276                         lastInsertFingerprints.put(sone.getId(), lastInsertFingerprint);
277
278                         allSones.put(sone.getId(), sone);
279                         storePosts(sone.getId(), posts);
280                         storePostReplies(sone.getId(), postReplies);
281                         storeAlbums(sone.getId(), topLevelAlbums);
282                         storeImages(sone.getId(), from(topLevelAlbums).transformAndConcat(Album.FLATTENER).transformAndConcat(Album.IMAGES).toList());
283                 } finally {
284                         lock.writeLock().unlock();
285                 }
286                 for (Post post : posts) {
287                         post.setKnown(true);
288                 }
289                 for (PostReply reply : postReplies) {
290                         reply.setKnown(true);
291                 }
292
293                 logger.info(String.format("Sone loaded successfully: %s", sone));
294         }
295
296         @Override
297         public String getLastInsertFingerprint(Sone sone) {
298                 lock.readLock().lock();
299                 try {
300                         if (!lastInsertFingerprints.containsKey(sone.getId())) {
301                                 return "";
302                         }
303                         return lastInsertFingerprints.get(sone.getId());
304                 } finally {
305                         lock.readLock().unlock();
306                 }
307         }
308
309         @Override
310         public void setLastInsertFingerprint(Sone sone, String lastInsertFingerprint) {
311                 lock.writeLock().lock();
312                 try {
313                         lastInsertFingerprints.put(sone.getId(), lastInsertFingerprint);
314                 } finally {
315                         lock.writeLock().unlock();
316                 }
317         }
318
319         /**
320          * Saves the database.
321          *
322          * @throws DatabaseException
323          *              if an error occurs while saving
324          */
325         @Override
326         public void save() throws DatabaseException {
327                 lock.writeLock().lock();
328                 try {
329                         saveKnownPosts();
330                         saveKnownPostReplies();
331                         for (Sone localSone : from(localSones).transform(soneLoader()).transform(Optionals.<Sone>get())) {
332                                 saveSone(localSone);
333                         }
334                 } finally {
335                         lock.writeLock().unlock();
336                 }
337         }
338
339         private synchronized void saveSone(Sone sone) {
340                 logger.log(Level.INFO, String.format("Saving Sone: %s", sone));
341                 try {
342                         /* save Sone into configuration. */
343                         String sonePrefix = "Sone/" + sone.getId();
344                         configuration.getLongValue(sonePrefix + "/Time").setValue(sone.getTime());
345                         configuration.getStringValue(sonePrefix + "/LastInsertFingerprint").setValue(lastInsertFingerprints.get(sone.getId()));
346
347                         /* save profile. */
348                         Profile profile = sone.getProfile();
349                         configuration.getStringValue(sonePrefix + "/Profile/FirstName").setValue(profile.getFirstName());
350                         configuration.getStringValue(sonePrefix + "/Profile/MiddleName").setValue(profile.getMiddleName());
351                         configuration.getStringValue(sonePrefix + "/Profile/LastName").setValue(profile.getLastName());
352                         configuration.getIntValue(sonePrefix + "/Profile/BirthDay").setValue(profile.getBirthDay());
353                         configuration.getIntValue(sonePrefix + "/Profile/BirthMonth").setValue(profile.getBirthMonth());
354                         configuration.getIntValue(sonePrefix + "/Profile/BirthYear").setValue(profile.getBirthYear());
355                         configuration.getStringValue(sonePrefix + "/Profile/Avatar").setValue(profile.getAvatar());
356
357                         /* save profile fields. */
358                         int fieldCounter = 0;
359                         for (Field profileField : profile.getFields()) {
360                                 String fieldPrefix = sonePrefix + "/Profile/Fields/" + fieldCounter++;
361                                 configuration.getStringValue(fieldPrefix + "/Name").setValue(profileField.getName());
362                                 configuration.getStringValue(fieldPrefix + "/Value").setValue(profileField.getValue());
363                         }
364                         configuration.getStringValue(sonePrefix + "/Profile/Fields/" + fieldCounter + "/Name").setValue(null);
365
366                         /* save posts. */
367                         int postCounter = 0;
368                         for (Post post : sone.getPosts()) {
369                                 String postPrefix = sonePrefix + "/Posts/" + postCounter++;
370                                 configuration.getStringValue(postPrefix + "/ID").setValue(post.getId());
371                                 configuration.getStringValue(postPrefix + "/Recipient").setValue(post.getRecipientId().orNull());
372                                 configuration.getLongValue(postPrefix + "/Time").setValue(post.getTime());
373                                 configuration.getStringValue(postPrefix + "/Text").setValue(post.getText());
374                         }
375                         configuration.getStringValue(sonePrefix + "/Posts/" + postCounter + "/ID").setValue(null);
376
377                         /* save replies. */
378                         int replyCounter = 0;
379                         for (PostReply reply : sone.getReplies()) {
380                                 String replyPrefix = sonePrefix + "/Replies/" + replyCounter++;
381                                 configuration.getStringValue(replyPrefix + "/ID").setValue(reply.getId());
382                                 configuration.getStringValue(replyPrefix + "/Post/ID").setValue(reply.getPostId());
383                                 configuration.getLongValue(replyPrefix + "/Time").setValue(reply.getTime());
384                                 configuration.getStringValue(replyPrefix + "/Text").setValue(reply.getText());
385                         }
386                         configuration.getStringValue(sonePrefix + "/Replies/" + replyCounter + "/ID").setValue(null);
387
388                         /* save post likes. */
389                         int postLikeCounter = 0;
390                         for (String postId : sone.getLikedPostIds()) {
391                                 configuration.getStringValue(sonePrefix + "/Likes/Post/" + postLikeCounter++ + "/ID").setValue(postId);
392                         }
393                         configuration.getStringValue(sonePrefix + "/Likes/Post/" + postLikeCounter + "/ID").setValue(null);
394
395                         /* save reply likes. */
396                         int replyLikeCounter = 0;
397                         for (String replyId : sone.getLikedReplyIds()) {
398                                 configuration.getStringValue(sonePrefix + "/Likes/Reply/" + replyLikeCounter++ + "/ID").setValue(replyId);
399                         }
400                         configuration.getStringValue(sonePrefix + "/Likes/Reply/" + replyLikeCounter + "/ID").setValue(null);
401
402                         /* save albums. first, collect in a flat structure, top-level first. */
403                         List<Album> albums = FluentIterable.from(sone.getRootAlbum().getAlbums()).transformAndConcat(Album.FLATTENER).toList();
404
405                         int albumCounter = 0;
406                         for (Album album : albums) {
407                                 String albumPrefix = sonePrefix + "/Albums/" + albumCounter++;
408                                 configuration.getStringValue(albumPrefix + "/ID").setValue(album.getId());
409                                 configuration.getStringValue(albumPrefix + "/Title").setValue(album.getTitle());
410                                 configuration.getStringValue(albumPrefix + "/Description").setValue(album.getDescription());
411                                 configuration.getStringValue(albumPrefix + "/Parent").setValue(album.getParent().equals(sone.getRootAlbum()) ? null : album.getParent().getId());
412                                 configuration.getStringValue(albumPrefix + "/AlbumImage").setValue(album.getAlbumImage() == null ? null : album.getAlbumImage().getId());
413                         }
414                         configuration.getStringValue(sonePrefix + "/Albums/" + albumCounter + "/ID").setValue(null);
415
416                         /* save images. */
417                         int imageCounter = 0;
418                         for (Album album : albums) {
419                                 for (Image image : album.getImages()) {
420                                         if (!image.isInserted()) {
421                                                 continue;
422                                         }
423                                         String imagePrefix = sonePrefix + "/Images/" + imageCounter++;
424                                         configuration.getStringValue(imagePrefix + "/ID").setValue(image.getId());
425                                         configuration.getStringValue(imagePrefix + "/Album").setValue(album.getId());
426                                         configuration.getStringValue(imagePrefix + "/Key").setValue(image.getKey());
427                                         configuration.getStringValue(imagePrefix + "/Title").setValue(image.getTitle());
428                                         configuration.getStringValue(imagePrefix + "/Description").setValue(image.getDescription());
429                                         configuration.getLongValue(imagePrefix + "/CreationTime").setValue(image.getCreationTime());
430                                         configuration.getIntValue(imagePrefix + "/Width").setValue(image.getWidth());
431                                         configuration.getIntValue(imagePrefix + "/Height").setValue(image.getHeight());
432                                 }
433                         }
434                         configuration.getStringValue(sonePrefix + "/Images/" + imageCounter + "/ID").setValue(null);
435
436                         /* save options. */
437                         configuration.getBooleanValue(sonePrefix + "/Options/AutoFollow").setValue(sone.getOptions().isAutoFollow());
438                         configuration.getBooleanValue(sonePrefix + "/Options/EnableSoneInsertNotifications").setValue(sone.getOptions().isSoneInsertNotificationEnabled());
439                         configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewSones").setValue(sone.getOptions().isShowNewSoneNotifications());
440                         configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewPosts").setValue(sone.getOptions().isShowNewPostNotifications());
441                         configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewReplies").setValue(sone.getOptions().isShowNewReplyNotifications());
442                         configuration.getStringValue(sonePrefix + "/Options/ShowCustomAvatars").setValue(sone.getOptions().getShowCustomAvatars().name());
443
444                         configuration.save();
445
446                         logger.log(Level.INFO, String.format("Sone %s saved.", sone));
447                 } catch (ConfigurationException ce1) {
448                         logger.log(Level.WARNING, String.format("Could not save Sone: %s", sone), ce1);
449                 }
450         }
451
452         //
453         // SERVICE METHODS
454         //
455
456         /** {@inheritDocs} */
457         @Override
458         protected void doStart() {
459                 memoryBookmarkDatabase.start();
460                 loadKnownPosts();
461                 loadKnownPostReplies();
462                 notifyStarted();
463         }
464
465         /** {@inheritDocs} */
466         @Override
467         protected void doStop() {
468                 try {
469                         memoryBookmarkDatabase.stop();
470                         save();
471                         notifyStopped();
472                 } catch (DatabaseException de1) {
473                         notifyFailed(de1);
474                 }
475         }
476
477         @Override
478         public SoneBuilder newSoneBuilder() {
479                 return new MemorySoneBuilder(this);
480         }
481
482         @Override
483         public void storeSone(Sone sone) {
484                 lock.writeLock().lock();
485                 try {
486                         removeSone(sone);
487
488                         allSones.put(sone.getId(), sone);
489                         storePosts(sone.getId(), sone.getPosts());
490                         storePostReplies(sone.getId(), sone.getReplies());
491                         storeAlbums(sone.getId(), toAllAlbums.apply(sone));
492                         storeImages(sone.getId(), toAllImages.apply(sone));
493                 } finally {
494                         lock.writeLock().unlock();
495                 }
496         }
497
498         private void storePosts(String soneId, Collection<Post> posts) {
499                 sonePosts.putAll(soneId, posts);
500                 for (Post post : posts) {
501                         allPosts.put(post.getId(), post);
502                 }
503         }
504
505         private void storePostReplies(String soneId, Collection<PostReply> postReplies) {
506                 sonePostReplies.putAll(soneId, postReplies);
507                 for (PostReply postReply : postReplies) {
508                         allPostReplies.put(postReply.getId(), postReply);
509                 }
510         }
511
512         private void storeAlbums(String soneId, Collection<Album> albums) {
513                 soneAlbums.putAll(soneId, albums);
514                 for (Album album : albums) {
515                         allAlbums.put(album.getId(), album);
516                 }
517         }
518
519         private void storeImages(String soneId, Collection<Image> images) {
520                 soneImages.putAll(soneId, images);
521                 for (Image image : images) {
522                         allImages.put(image.getId(), image);
523                 }
524         }
525
526         @Override
527         public void removeSone(Sone sone) {
528                 lock.writeLock().lock();
529                 try {
530                         allSones.remove(sone.getId());
531                         Collection<Post> removedPosts = sonePosts.removeAll(sone.getId());
532                         for (Post removedPost : removedPosts) {
533                                 allPosts.remove(removedPost.getId());
534                         }
535                         Collection<PostReply> removedPostReplies =
536                                         sonePostReplies.removeAll(sone.getId());
537                         for (PostReply removedPostReply : removedPostReplies) {
538                                 allPostReplies.remove(removedPostReply.getId());
539                         }
540                         Collection<Album> removedAlbums =
541                                         soneAlbums.removeAll(sone.getId());
542                         for (Album removedAlbum : removedAlbums) {
543                                 allAlbums.remove(removedAlbum.getId());
544                         }
545                         Collection<Image> removedImages =
546                                         soneImages.removeAll(sone.getId());
547                         for (Image removedImage : removedImages) {
548                                 allImages.remove(removedImage.getId());
549                         }
550                 } finally {
551                         lock.writeLock().unlock();
552                 }
553         }
554
555         @Override
556         public Function<String, Optional<Sone>> soneLoader() {
557                 return new Function<String, Optional<Sone>>() {
558                         @Override
559                         public Optional<Sone> apply(String soneId) {
560                                 return getSone(soneId);
561                         }
562                 };
563         }
564
565         @Override
566         public Optional<Sone> getSone(String soneId) {
567                 lock.readLock().lock();
568                 try {
569                         return fromNullable(allSones.get(soneId));
570                 } finally {
571                         lock.readLock().unlock();
572                 }
573         }
574
575         @Override
576         public Collection<Sone> getSones() {
577                 lock.readLock().lock();
578                 try {
579                         return new HashSet<Sone>(allSones.values());
580                 } finally {
581                         lock.readLock().unlock();
582                 }
583         }
584
585         @Override
586         public Collection<Sone> getLocalSones() {
587                 lock.readLock().lock();
588                 try {
589                         return from(allSones.values()).filter(LOCAL_SONE_FILTER).toSet();
590                 } finally {
591                         lock.readLock().unlock();
592                 }
593         }
594
595         @Override
596         public Collection<Sone> getRemoteSones() {
597                 lock.readLock().lock();
598                 try {
599                         return from(allSones.values())
600                                         .filter(not(LOCAL_SONE_FILTER)) .toSet();
601                 } finally {
602                         lock.readLock().unlock();
603                 }
604         }
605
606         @Override
607         public Collection<String> getFriends(Sone localSone) {
608                 if (!localSone.isLocal()) {
609                         return Collections.emptySet();
610                 }
611                 return memoryFriendDatabase.getFriends(localSone.getId());
612         }
613
614         @Override
615         public boolean isFriend(Sone localSone, String friendSoneId) {
616                 if (!localSone.isLocal()) {
617                         return false;
618                 }
619                 return memoryFriendDatabase.isFriend(localSone.getId(), friendSoneId);
620         }
621
622         @Override
623         public void addFriend(Sone localSone, String friendSoneId) {
624                 if (!localSone.isLocal()) {
625                         return;
626                 }
627                 memoryFriendDatabase.addFriend(localSone.getId(), friendSoneId);
628         }
629
630         @Override
631         public void removeFriend(Sone localSone, String friendSoneId) {
632                 if (!localSone.isLocal()) {
633                         return;
634                 }
635                 memoryFriendDatabase.removeFriend(localSone.getId(), friendSoneId);
636         }
637
638         //
639         // POSTPROVIDER METHODS
640         //
641
642         /** {@inheritDocs} */
643         @Override
644         public Optional<Post> getPost(String postId) {
645                 lock.readLock().lock();
646                 try {
647                         return fromNullable(allPosts.get(postId));
648                 } finally {
649                         lock.readLock().unlock();
650                 }
651         }
652
653         /** {@inheritDocs} */
654         @Override
655         public Collection<Post> getPosts(String soneId) {
656                 return new HashSet<Post>(getPostsFrom(soneId));
657         }
658
659         /** {@inheritDocs} */
660         @Override
661         public Collection<Post> getDirectedPosts(final String recipientId) {
662                 lock.readLock().lock();
663                 try {
664                         return from(sonePosts.values()).filter(new Predicate<Post>() {
665                                 @Override
666                                 public boolean apply(Post post) {
667                                         return post.getRecipientId().asSet().contains(recipientId);
668                                 }
669                         }).toSet();
670                 } finally {
671                         lock.readLock().unlock();
672                 }
673         }
674
675         //
676         // POSTBUILDERFACTORY METHODS
677         //
678
679         /** {@inheritDocs} */
680         @Override
681         public PostBuilder newPostBuilder() {
682                 return new MemoryPostBuilder(this, soneProvider);
683         }
684
685         //
686         // POSTSTORE METHODS
687         //
688
689         /** {@inheritDocs} */
690         @Override
691         public void storePost(Post post) {
692                 checkNotNull(post, "post must not be null");
693                 lock.writeLock().lock();
694                 try {
695                         allPosts.put(post.getId(), post);
696                         getPostsFrom(post.getSone().getId()).add(post);
697                 } finally {
698                         lock.writeLock().unlock();
699                 }
700         }
701
702         /** {@inheritDocs} */
703         @Override
704         public void removePost(Post post) {
705                 checkNotNull(post, "post must not be null");
706                 lock.writeLock().lock();
707                 try {
708                         allPosts.remove(post.getId());
709                         getPostsFrom(post.getSone().getId()).remove(post);
710                         post.getSone().removePost(post);
711                 } finally {
712                         lock.writeLock().unlock();
713                 }
714         }
715
716         //
717         // POSTREPLYPROVIDER METHODS
718         //
719
720         /** {@inheritDocs} */
721         @Override
722         public Optional<PostReply> getPostReply(String id) {
723                 lock.readLock().lock();
724                 try {
725                         return fromNullable(allPostReplies.get(id));
726                 } finally {
727                         lock.readLock().unlock();
728                 }
729         }
730
731         /** {@inheritDocs} */
732         @Override
733         public List<PostReply> getReplies(final String postId) {
734                 lock.readLock().lock();
735                 try {
736                         return from(allPostReplies.values())
737                                         .filter(new Predicate<PostReply>() {
738                                                 @Override
739                                                 public boolean apply(PostReply postReply) {
740                                                         return postReply.getPostId().equals(postId);
741                                                 }
742                                         }).toSortedList(TIME_COMPARATOR);
743                 } finally {
744                         lock.readLock().unlock();
745                 }
746         }
747
748         //
749         // POSTREPLYBUILDERFACTORY METHODS
750         //
751
752         /** {@inheritDocs} */
753         @Override
754         public PostReplyBuilder newPostReplyBuilder() {
755                 return new MemoryPostReplyBuilder(this, soneProvider);
756         }
757
758         //
759         // POSTREPLYSTORE METHODS
760         //
761
762         /** {@inheritDocs} */
763         @Override
764         public void storePostReply(PostReply postReply) {
765                 lock.writeLock().lock();
766                 try {
767                         allPostReplies.put(postReply.getId(), postReply);
768                 } finally {
769                         lock.writeLock().unlock();
770                 }
771         }
772
773         /** {@inheritDocs} */
774         @Override
775         public void removePostReply(PostReply postReply) {
776                 lock.writeLock().lock();
777                 try {
778                         allPostReplies.remove(postReply.getId());
779                 } finally {
780                         lock.writeLock().unlock();
781                 }
782         }
783
784         //
785         // ALBUMPROVDER METHODS
786         //
787
788         @Override
789         public Optional<Album> getAlbum(String albumId) {
790                 lock.readLock().lock();
791                 try {
792                         return fromNullable(allAlbums.get(albumId));
793                 } finally {
794                         lock.readLock().unlock();
795                 }
796         }
797
798         //
799         // ALBUMBUILDERFACTORY METHODS
800         //
801
802         @Override
803         public AlbumBuilder newAlbumBuilder() {
804                 return new AlbumBuilderImpl();
805         }
806
807         //
808         // ALBUMSTORE METHODS
809         //
810
811         @Override
812         public void storeAlbum(Album album) {
813                 lock.writeLock().lock();
814                 try {
815                         allAlbums.put(album.getId(), album);
816                         soneAlbums.put(album.getSone().getId(), album);
817                 } finally {
818                         lock.writeLock().unlock();
819                 }
820         }
821
822         @Override
823         public void removeAlbum(Album album) {
824                 lock.writeLock().lock();
825                 try {
826                         allAlbums.remove(album.getId());
827                         soneAlbums.remove(album.getSone().getId(), album);
828                 } finally {
829                         lock.writeLock().unlock();
830                 }
831         }
832
833         //
834         // IMAGEPROVIDER METHODS
835         //
836
837         @Override
838         public Optional<Image> getImage(String imageId) {
839                 lock.readLock().lock();
840                 try {
841                         return fromNullable(allImages.get(imageId));
842                 } finally {
843                         lock.readLock().unlock();
844                 }
845         }
846
847         //
848         // IMAGEBUILDERFACTORY METHODS
849         //
850
851         @Override
852         public ImageBuilder newImageBuilder() {
853                 return new ImageBuilderImpl();
854         }
855
856         //
857         // IMAGESTORE METHODS
858         //
859
860         @Override
861         public void storeImage(Image image) {
862                 lock.writeLock().lock();
863                 try {
864                         allImages.put(image.getId(), image);
865                         soneImages.put(image.getSone().getId(), image);
866                 } finally {
867                         lock.writeLock().unlock();
868                 }
869         }
870
871         @Override
872         public void removeImage(Image image) {
873                 lock.writeLock().lock();
874                 try {
875                         allImages.remove(image.getId());
876                         soneImages.remove(image.getSone().getId(), image);
877                 } finally {
878                         lock.writeLock().unlock();
879                 }
880         }
881
882         @Override
883         public void bookmarkPost(Post post) {
884                 memoryBookmarkDatabase.bookmarkPost(post);
885         }
886
887         @Override
888         public void unbookmarkPost(Post post) {
889                 memoryBookmarkDatabase.unbookmarkPost(post);
890         }
891
892         @Override
893         public boolean isPostBookmarked(Post post) {
894                 return memoryBookmarkDatabase.isPostBookmarked(post);
895         }
896
897         @Override
898         public Set<Post> getBookmarkedPosts() {
899                 return memoryBookmarkDatabase.getBookmarkedPosts();
900         }
901
902         //
903         // PACKAGE-PRIVATE METHODS
904         //
905
906         /**
907          * Returns whether the given post is known.
908          *
909          * @param post
910          *              The post
911          * @return {@code true} if the post is known, {@code false} otherwise
912          */
913         boolean isPostKnown(Post post) {
914                 lock.readLock().lock();
915                 try {
916                         return knownPosts.contains(post.getId());
917                 } finally {
918                         lock.readLock().unlock();
919                 }
920         }
921
922         /**
923          * Sets whether the given post is known.
924          *
925          * @param post
926          *              The post
927          * @param known
928          *              {@code true} if the post is known, {@code false} otherwise
929          */
930         void setPostKnown(Post post, boolean known) {
931                 lock.writeLock().lock();
932                 try {
933                         if (known) {
934                                 knownPosts.add(post.getId());
935                         } else {
936                                 knownPosts.remove(post.getId());
937                         }
938                 } finally {
939                         lock.writeLock().unlock();
940                 }
941         }
942
943         /**
944          * Returns whether the given post reply is known.
945          *
946          * @param postReply
947          *              The post reply
948          * @return {@code true} if the given post reply is known, {@code false}
949          *         otherwise
950          */
951         boolean isPostReplyKnown(PostReply postReply) {
952                 lock.readLock().lock();
953                 try {
954                         return knownPostReplies.contains(postReply.getId());
955                 } finally {
956                         lock.readLock().unlock();
957                 }
958         }
959
960         /**
961          * Sets whether the given post reply is known.
962          *
963          * @param postReply
964          *              The post reply
965          * @param known
966          *              {@code true} if the post reply is known, {@code false} otherwise
967          */
968         void setPostReplyKnown(PostReply postReply, boolean known) {
969                 lock.writeLock().lock();
970                 try {
971                         if (known) {
972                                 knownPostReplies.add(postReply.getId());
973                         } else {
974                                 knownPostReplies.remove(postReply.getId());
975                         }
976                 } finally {
977                         lock.writeLock().unlock();
978                 }
979         }
980
981         //
982         // PRIVATE METHODS
983         //
984
985         /**
986          * Gets all posts for the given Sone, creating a new collection if there is
987          * none yet.
988          *
989          * @param soneId
990          *              The ID of the Sone to get the posts for
991          * @return All posts
992          */
993         private Collection<Post> getPostsFrom(String soneId) {
994                 lock.readLock().lock();
995                 try {
996                         return sonePosts.get(soneId);
997                 } finally {
998                         lock.readLock().unlock();
999                 }
1000         }
1001
1002         /** Loads the known posts. */
1003         private void loadKnownPosts() {
1004                 Set<String> knownPosts = configurationLoader.loadKnownPosts();
1005                 lock.writeLock().lock();
1006                 try {
1007                         this.knownPosts.clear();
1008                         this.knownPosts.addAll(knownPosts);
1009                 } finally {
1010                         lock.writeLock().unlock();
1011                 }
1012         }
1013
1014         /**
1015          * Saves the known posts to the configuration.
1016          *
1017          * @throws DatabaseException
1018          *              if a configuration error occurs
1019          */
1020         private void saveKnownPosts() throws DatabaseException {
1021                 lock.readLock().lock();
1022                 try {
1023                         int postCounter = 0;
1024                         for (String knownPostId : knownPosts) {
1025                                 configuration.getStringValue("KnownPosts/" + postCounter++ + "/ID").setValue(
1026                                                 knownPostId);
1027                         }
1028                         configuration.getStringValue("KnownPosts/" + postCounter + "/ID").setValue(null);
1029                 } catch (ConfigurationException ce1) {
1030                         throw new DatabaseException("Could not save database.", ce1);
1031                 } finally {
1032                         lock.readLock().unlock();
1033                 }
1034         }
1035
1036         /** Loads the known post replies. */
1037         private void loadKnownPostReplies() {
1038                 Set<String> knownPostReplies = configurationLoader.loadKnownPostReplies();
1039                 lock.writeLock().lock();
1040                 try {
1041                         this.knownPostReplies.clear();
1042                         this.knownPostReplies.addAll(knownPostReplies);
1043                 } finally {
1044                         lock.writeLock().unlock();
1045                 }
1046         }
1047
1048         /**
1049          * Saves the known post replies to the configuration.
1050          *
1051          * @throws DatabaseException
1052          *              if a configuration error occurs
1053          */
1054         private void saveKnownPostReplies() throws DatabaseException {
1055                 lock.readLock().lock();
1056                 try {
1057                         int replyCounter = 0;
1058                         for (String knownReplyId : knownPostReplies) {
1059                                 configuration.getStringValue("KnownReplies/" + replyCounter++ + "/ID").setValue(
1060                                                 knownReplyId);
1061                         }
1062                         configuration.getStringValue("KnownReplies/" + replyCounter + "/ID").setValue(null);
1063                 } catch (ConfigurationException ce1) {
1064                         throw new DatabaseException("Could not save database.", ce1);
1065                 } finally {
1066                         lock.readLock().unlock();
1067                 }
1068         }
1069
1070 }