Remove unnecessary method.
[Sone.git] / src / main / java / net / pterodactylus / sone / core / Core.java
index 5843b30..72e4090 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - Core.java - Copyright © 2010–2012 David Roden
+ * Sone - Core.java - Copyright © 2010–2013 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
 
 package net.pterodactylus.sone.core;
 
-import java.net.MalformedURLException;
-import java.util.ArrayList;
-import java.util.Collections;
+import static com.google.common.base.Optional.fromNullable;
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.primitives.Longs.tryParse;
+import static java.lang.String.format;
+import static java.util.logging.Level.WARNING;
+import static java.util.logging.Logger.getLogger;
+
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -28,12 +34,30 @@ import java.util.Map.Entry;
 import java.util.Set;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
-import net.pterodactylus.sone.core.Options.DefaultOption;
-import net.pterodactylus.sone.core.Options.Option;
-import net.pterodactylus.sone.core.Options.OptionWatcher;
+import net.pterodactylus.sone.core.ConfigurationSoneParser.InvalidAlbumFound;
+import net.pterodactylus.sone.core.ConfigurationSoneParser.InvalidImageFound;
+import net.pterodactylus.sone.core.ConfigurationSoneParser.InvalidParentAlbumFound;
+import net.pterodactylus.sone.core.ConfigurationSoneParser.InvalidPostFound;
+import net.pterodactylus.sone.core.ConfigurationSoneParser.InvalidPostReplyFound;
+import net.pterodactylus.sone.core.SoneChangeDetector.PostProcessor;
+import net.pterodactylus.sone.core.SoneChangeDetector.PostReplyProcessor;
+import net.pterodactylus.sone.core.event.ImageInsertFinishedEvent;
+import net.pterodactylus.sone.core.event.MarkPostKnownEvent;
+import net.pterodactylus.sone.core.event.MarkPostReplyKnownEvent;
+import net.pterodactylus.sone.core.event.MarkSoneKnownEvent;
+import net.pterodactylus.sone.core.event.NewPostFoundEvent;
+import net.pterodactylus.sone.core.event.NewPostReplyFoundEvent;
+import net.pterodactylus.sone.core.event.NewSoneFoundEvent;
+import net.pterodactylus.sone.core.event.PostRemovedEvent;
+import net.pterodactylus.sone.core.event.PostReplyRemovedEvent;
+import net.pterodactylus.sone.core.event.SoneLockedEvent;
+import net.pterodactylus.sone.core.event.SoneRemovedEvent;
+import net.pterodactylus.sone.core.event.SoneUnlockedEvent;
 import net.pterodactylus.sone.data.Album;
 import net.pterodactylus.sone.data.Client;
 import net.pterodactylus.sone.data.Image;
@@ -46,52 +70,63 @@ import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.data.Sone.ShowCustomAvatars;
 import net.pterodactylus.sone.data.Sone.SoneStatus;
 import net.pterodactylus.sone.data.TemporaryImage;
-import net.pterodactylus.sone.data.impl.PostImpl;
-import net.pterodactylus.sone.fcp.FcpInterface;
-import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired;
+import net.pterodactylus.sone.database.AlbumBuilder;
+import net.pterodactylus.sone.database.Database;
+import net.pterodactylus.sone.database.DatabaseException;
+import net.pterodactylus.sone.database.ImageBuilder;
+import net.pterodactylus.sone.database.PostBuilder;
+import net.pterodactylus.sone.database.PostProvider;
+import net.pterodactylus.sone.database.PostReplyBuilder;
+import net.pterodactylus.sone.database.PostReplyProvider;
+import net.pterodactylus.sone.database.SoneBuilder;
+import net.pterodactylus.sone.database.SoneProvider;
 import net.pterodactylus.sone.freenet.wot.Identity;
-import net.pterodactylus.sone.freenet.wot.IdentityListener;
 import net.pterodactylus.sone.freenet.wot.IdentityManager;
 import net.pterodactylus.sone.freenet.wot.OwnIdentity;
+import net.pterodactylus.sone.freenet.wot.event.IdentityAddedEvent;
+import net.pterodactylus.sone.freenet.wot.event.IdentityRemovedEvent;
+import net.pterodactylus.sone.freenet.wot.event.IdentityUpdatedEvent;
+import net.pterodactylus.sone.freenet.wot.event.OwnIdentityAddedEvent;
+import net.pterodactylus.sone.freenet.wot.event.OwnIdentityRemovedEvent;
 import net.pterodactylus.sone.main.SonePlugin;
 import net.pterodactylus.util.config.Configuration;
 import net.pterodactylus.util.config.ConfigurationException;
-import net.pterodactylus.util.logging.Logging;
-import net.pterodactylus.util.number.Numbers;
 import net.pterodactylus.util.service.AbstractService;
 import net.pterodactylus.util.thread.NamedThreadFactory;
-import net.pterodactylus.util.thread.Ticker;
-import net.pterodactylus.util.validation.EqualityValidator;
-import net.pterodactylus.util.validation.IntegerRangeValidator;
-import net.pterodactylus.util.validation.OrValidator;
-import net.pterodactylus.util.validation.Validation;
-import net.pterodactylus.util.version.Version;
-import freenet.keys.FreenetURI;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Optional;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Multimaps;
+import com.google.common.eventbus.EventBus;
+import com.google.common.eventbus.Subscribe;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
 
 /**
  * The Sone core.
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
-public class Core extends AbstractService implements IdentityListener, UpdateListener, SoneProvider, PostProvider, SoneInsertListener, ImageInsertListener {
+@Singleton
+public class Core extends AbstractService implements SoneProvider, PostProvider, PostReplyProvider {
 
        /** The logger. */
-       private static final Logger logger = Logging.getLogger(Core.class);
+       private static final Logger logger = getLogger("Sone.Core");
 
        /** The start time. */
        private final long startupTime = System.currentTimeMillis();
 
-       /** The options. */
-       private final Options options = new Options();
-
        /** The preferences. */
-       private final Preferences preferences = new Preferences(options);
+       private final Preferences preferences;
 
-       /** The core listener manager. */
-       private final CoreListenerManager coreListenerManager = new CoreListenerManager(this);
+       /** The event bus. */
+       private final EventBus eventBus;
 
        /** The configuration. */
-       private Configuration configuration;
+       private final Configuration configuration;
 
        /** Whether we’re currently saving the configuration. */
        private boolean storingConfiguration = false;
@@ -117,65 +152,35 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
        /** The trust updater. */
        private final WebOfTrustUpdater webOfTrustUpdater;
 
-       /** The FCP interface. */
-       private volatile FcpInterface fcpInterface;
-
        /** The times Sones were followed. */
-       private final Map<Sone, Long> soneFollowingTimes = new HashMap<Sone, Long>();
+       private final Map<String, Long> soneFollowingTimes = new HashMap<String, Long>();
 
        /** Locked local Sones. */
        /* synchronize on itself. */
        private final Set<Sone> lockedSones = new HashSet<Sone>();
 
        /** Sone inserters. */
-       /* synchronize access on this on localSones. */
+       /* synchronize access on this on sones. */
        private final Map<Sone, SoneInserter> soneInserters = new HashMap<Sone, SoneInserter>();
 
        /** Sone rescuers. */
-       /* synchronize access on this on localSones. */
+       /* synchronize access on this on sones. */
        private final Map<Sone, SoneRescuer> soneRescuers = new HashMap<Sone, SoneRescuer>();
 
-       /** All local Sones. */
-       /* synchronize access on this on itself. */
-       private final Map<String, Sone> localSones = new HashMap<String, Sone>();
-
-       /** All remote Sones. */
-       /* synchronize access on this on itself. */
-       private final Map<String, Sone> remoteSones = new HashMap<String, Sone>();
-
        /** All known Sones. */
        private final Set<String> knownSones = new HashSet<String>();
 
-       /** All posts. */
-       private final Map<String, Post> posts = new HashMap<String, Post>();
-
-       /** All known posts. */
-       private final Set<String> knownPosts = new HashSet<String>();
-
-       /** All replies. */
-       private final Map<String, PostReply> replies = new HashMap<String, PostReply>();
-
-       /** All known replies. */
-       private final Set<String> knownReplies = new HashSet<String>();
-
-       /** All bookmarked posts. */
-       /* synchronize access on itself. */
-       private final Set<String> bookmarkedPosts = new HashSet<String>();
+       /** The post database. */
+       private final Database database;
 
        /** Trusted identities, sorted by own identities. */
-       private final Map<OwnIdentity, Set<Identity>> trustedIdentities = Collections.synchronizedMap(new HashMap<OwnIdentity, Set<Identity>>());
-
-       /** All known albums. */
-       private final Map<String, Album> albums = new HashMap<String, Album>();
-
-       /** All known images. */
-       private final Map<String, Image> images = new HashMap<String, Image>();
+       private final Multimap<OwnIdentity, Identity> trustedIdentities = Multimaps.synchronizedSetMultimap(HashMultimap.<OwnIdentity, Identity>create());
 
        /** All temporary images. */
        private final Map<String, TemporaryImage> temporaryImages = new HashMap<String, TemporaryImage>();
 
        /** Ticker for threads that mark own elements as known. */
-       private final Ticker localElementTicker = new Ticker();
+       private final ScheduledExecutorService localElementTicker = Executors.newScheduledThreadPool(1);
 
        /** The time the configuration was last touched. */
        private volatile long lastConfigurationUpdate;
@@ -191,40 +196,39 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
         *            The identity manager
         * @param webOfTrustUpdater
         *            The WebOfTrust updater
+        * @param eventBus
+        *            The event bus
+        * @param database
+        *            The database
         */
-       public Core(Configuration configuration, FreenetInterface freenetInterface, IdentityManager identityManager, WebOfTrustUpdater webOfTrustUpdater) {
+       @Inject
+       public Core(Configuration configuration, FreenetInterface freenetInterface, IdentityManager identityManager, WebOfTrustUpdater webOfTrustUpdater, EventBus eventBus, Database database) {
                super("Sone Core");
                this.configuration = configuration;
                this.freenetInterface = freenetInterface;
                this.identityManager = identityManager;
-               this.soneDownloader = new SoneDownloader(this, freenetInterface);
-               this.imageInserter = new ImageInserter(this, freenetInterface);
-               this.updateChecker = new UpdateChecker(freenetInterface);
+               this.soneDownloader = new SoneDownloaderImpl(this, freenetInterface);
+               this.imageInserter = new ImageInserter(freenetInterface, freenetInterface.new InsertTokenSupplier());
+               this.updateChecker = new UpdateChecker(eventBus, freenetInterface);
                this.webOfTrustUpdater = webOfTrustUpdater;
+               this.eventBus = eventBus;
+               this.database = database;
+               preferences = new Preferences(eventBus);
        }
 
-       //
-       // LISTENER MANAGEMENT
-       //
-
-       /**
-        * Adds a new core listener.
-        *
-        * @param coreListener
-        *            The listener to add
-        */
-       public void addCoreListener(CoreListener coreListener) {
-               coreListenerManager.addListener(coreListener);
-       }
-
-       /**
-        * Removes a core listener.
-        *
-        * @param coreListener
-        *            The listener to remove
-        */
-       public void removeCoreListener(CoreListener coreListener) {
-               coreListenerManager.removeListener(coreListener);
+       @VisibleForTesting
+       protected Core(Configuration configuration, FreenetInterface freenetInterface, IdentityManager identityManager, SoneDownloader soneDownloader, ImageInserter imageInserter, UpdateChecker updateChecker, WebOfTrustUpdater webOfTrustUpdater, EventBus eventBus, Database database) {
+               super("Sone Core");
+               this.configuration = configuration;
+               this.freenetInterface = freenetInterface;
+               this.identityManager = identityManager;
+               this.soneDownloader = soneDownloader;
+               this.imageInserter = imageInserter;
+               this.updateChecker = updateChecker;
+               this.webOfTrustUpdater = webOfTrustUpdater;
+               this.eventBus = eventBus;
+               this.database = database;
+               preferences = new Preferences(eventBus);
        }
 
        //
@@ -241,18 +245,6 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
        }
 
        /**
-        * Sets the configuration to use. This will automatically save the current
-        * configuration to the given configuration.
-        *
-        * @param configuration
-        *            The new configuration to use
-        */
-       public void setConfiguration(Configuration configuration) {
-               this.configuration = configuration;
-               touchConfiguration();
-       }
-
-       /**
         * Returns the options used by the core.
         *
         * @return The options of the core
@@ -280,16 +272,6 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
        }
 
        /**
-        * Sets the FCP interface to use.
-        *
-        * @param fcpInterface
-        *            The FCP interface to use
-        */
-       public void setFcpInterface(FcpInterface fcpInterface) {
-               this.fcpInterface = fcpInterface;
-       }
-
-       /**
         * Returns the Sone rescuer for the given local Sone.
         *
         * @param sone
@@ -297,8 +279,9 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
         * @return The Sone rescuer for the given Sone
         */
        public SoneRescuer getSoneRescuer(Sone sone) {
-               Validation.begin().isNotNull("Sone", sone).check().is("Local Sone", isLocalSone(sone)).check();
-               synchronized (localSones) {
+               checkNotNull(sone, "sone must not be null");
+               checkArgument(sone.isLocal(), "sone must be local");
+               synchronized (soneRescuers) {
                        SoneRescuer soneRescuer = soneRescuers.get(sone);
                        if (soneRescuer == null) {
                                soneRescuer = new SoneRescuer(this, soneDownloader, sone);
@@ -322,29 +305,16 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
                }
        }
 
-       /**
-        * Returns all Sones, remote and local.
-        *
-        * @return All Sones
-        */
-       public Set<Sone> getSones() {
-               Set<Sone> allSones = new HashSet<Sone>();
-               allSones.addAll(getLocalSones());
-               allSones.addAll(getRemoteSones());
-               return allSones;
+       public SoneBuilder soneBuilder() {
+               return database.newSoneBuilder();
        }
 
        /**
-        * Returns the Sone with the given ID, regardless whether it’s local or
-        * remote.
-        *
-        * @param id
-        *            The ID of the Sone to get
-        * @return The Sone with the given ID, or {@code null} if there is no such
-        *         Sone
+        * {@inheritDocs}
         */
-       public Sone getSone(String id) {
-               return getSone(id, true);
+       @Override
+       public Collection<Sone> getSones() {
+               return database.getSones();
        }
 
        /**
@@ -353,80 +323,20 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
         *
         * @param id
         *            The ID of the Sone to get
-        * @param create
-        *            {@code true} to create a new Sone if none exists,
-        *            {@code false} to return {@code null} if a Sone with the given
-        *            ID does not exist
         * @return The Sone with the given ID, or {@code null} if there is no such
         *         Sone
         */
        @Override
-       public Sone getSone(String id, boolean create) {
-               if (isLocalSone(id)) {
-                       return getLocalSone(id);
-               }
-               return getRemoteSone(id, create);
-       }
-
-       /**
-        * Checks whether the core knows a Sone with the given ID.
-        *
-        * @param id
-        *            The ID of the Sone
-        * @return {@code true} if there is a Sone with the given ID, {@code false}
-        *         otherwise
-        */
-       public boolean hasSone(String id) {
-               return isLocalSone(id) || isRemoteSone(id);
-       }
-
-       /**
-        * Returns whether the given Sone is a local Sone.
-        *
-        * @param sone
-        *            The Sone to check for its locality
-        * @return {@code true} if the given Sone is local, {@code false} otherwise
-        */
-       public boolean isLocalSone(Sone sone) {
-               synchronized (localSones) {
-                       return localSones.containsKey(sone.getId());
-               }
-       }
-
-       /**
-        * Returns whether the given ID is the ID of a local Sone.
-        *
-        * @param id
-        *            The Sone ID to check for its locality
-        * @return {@code true} if the given ID is a local Sone, {@code false}
-        *         otherwise
-        */
-       public boolean isLocalSone(String id) {
-               synchronized (localSones) {
-                       return localSones.containsKey(id);
-               }
-       }
-
-       /**
-        * Returns all local Sones.
-        *
-        * @return All local Sones
-        */
-       public Set<Sone> getLocalSones() {
-               synchronized (localSones) {
-                       return new HashSet<Sone>(localSones.values());
-               }
+       public Optional<Sone> getSone(String id) {
+               return database.getSone(id);
        }
 
        /**
-        * Returns the local Sone with the given ID.
-        *
-        * @param id
-        *            The ID of the Sone to get
-        * @return The Sone with the given ID
+        * {@inheritDocs}
         */
-       public Sone getLocalSone(String id) {
-               return getLocalSone(id, true);
+       @Override
+       public Collection<Sone> getLocalSones() {
+               return database.getLocalSones();
        }
 
        /**
@@ -434,80 +344,34 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
         *
         * @param id
         *            The ID of the Sone
-        * @param create
-        *            {@code true} to create a new Sone if none exists,
-        *            {@code false} to return null if none exists
         * @return The Sone with the given ID, or {@code null}
         */
-       public Sone getLocalSone(String id, boolean create) {
-               synchronized (localSones) {
-                       Sone sone = localSones.get(id);
-                       if ((sone == null) && create) {
-                               sone = new Sone(id);
-                               localSones.put(id, sone);
-                       }
-                       return sone;
+       public Sone getLocalSone(String id) {
+               Optional<Sone> sone = database.getSone(id);
+               if (sone.isPresent() && sone.get().isLocal()) {
+                       return sone.get();
                }
+               return null;
        }
 
        /**
-        * Returns all remote Sones.
-        *
-        * @return All remote Sones
+        * {@inheritDocs}
         */
-       public Set<Sone> getRemoteSones() {
-               synchronized (remoteSones) {
-                       return new HashSet<Sone>(remoteSones.values());
-               }
+       @Override
+       public Collection<Sone> getRemoteSones() {
+               return database.getRemoteSones();
        }
 
        /**
         * Returns the remote Sone with the given ID.
         *
+        *
         * @param id
         *            The ID of the remote Sone to get
-        * @param create
-        *            {@code true} to always create a Sone, {@code false} to return
-        *            {@code null} if no Sone with the given ID exists
         * @return The Sone with the given ID
         */
-       public Sone getRemoteSone(String id, boolean create) {
-               synchronized (remoteSones) {
-                       Sone sone = remoteSones.get(id);
-                       if ((sone == null) && create && (id != null) && (id.length() == 43)) {
-                               sone = new Sone(id);
-                               remoteSones.put(id, sone);
-                       }
-                       return sone;
-               }
-       }
-
-       /**
-        * Returns whether the given Sone is a remote Sone.
-        *
-        * @param sone
-        *            The Sone to check
-        * @return {@code true} if the given Sone is a remote Sone, {@code false}
-        *         otherwise
-        */
-       public boolean isRemoteSone(Sone sone) {
-               synchronized (remoteSones) {
-                       return remoteSones.containsKey(sone.getId());
-               }
-       }
-
-       /**
-        * Returns whether the Sone with the given ID is a remote Sone.
-        *
-        * @param id
-        *            The ID of the Sone to check
-        * @return {@code true} if the Sone with the given ID is a remote Sone,
-        *         {@code false} otherwise
-        */
-       public boolean isRemoteSone(String id) {
-               synchronized (remoteSones) {
-                       return remoteSones.containsKey(id);
-               }
+       public Sone getRemoteSone(String id) {
+               return database.getSone(id).orNull();
        }
 
        /**
@@ -519,7 +383,7 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
         *         {@code false} otherwise
         */
        public boolean isModifiedSone(Sone sone) {
-               return (soneInserters.containsKey(sone)) ? soneInserters.get(sone).isModified() : false;
+               return soneInserters.containsKey(sone) && soneInserters.get(sone).isModified();
        }
 
        /**
@@ -532,135 +396,67 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
         */
        public long getSoneFollowingTime(Sone sone) {
                synchronized (soneFollowingTimes) {
-                       if (soneFollowingTimes.containsKey(sone)) {
-                               return soneFollowingTimes.get(sone);
-                       }
-                       return Long.MAX_VALUE;
+                       return Optional.fromNullable(soneFollowingTimes.get(sone.getId())).or(Long.MAX_VALUE);
                }
        }
 
        /**
-        * Returns whether the target Sone is trusted by the origin Sone.
+        * Returns a post builder.
         *
-        * @param origin
-        *            The origin Sone
-        * @param target
-        *            The target Sone
-        * @return {@code true} if the target Sone is trusted by the origin Sone
+        * @return A new post builder
         */
-       public boolean isSoneTrusted(Sone origin, Sone target) {
-               Validation.begin().isNotNull("Origin", origin).isNotNull("Target", target).check().isInstanceOf("Origin’s OwnIdentity", origin.getIdentity(), OwnIdentity.class).check();
-               return trustedIdentities.containsKey(origin.getIdentity()) && trustedIdentities.get(origin.getIdentity()).contains(target.getIdentity());
+       public PostBuilder postBuilder() {
+               return database.newPostBuilder();
        }
 
        /**
-        * Returns the post with the given ID.
-        *
-        * @param postId
-        *            The ID of the post to get
-        * @return The post with the given ID, or a new post with the given ID
+        * {@inheritDoc}
         */
-       public Post getPost(String postId) {
-               return getPost(postId, true);
+       @Override
+       public Optional<Post> getPost(String postId) {
+               return database.getPost(postId);
        }
 
        /**
-        * Returns the post with the given ID, optionally creating a new post.
-        *
-        * @param postId
-        *            The ID of the post to get
-        * @param create
-        *            {@code true} it create a new post if no post with the given ID
-        *            exists, {@code false} to return {@code null}
-        * @return The post, or {@code null} if there is no such post
+        * {@inheritDocs}
         */
        @Override
-       public Post getPost(String postId, boolean create) {
-               synchronized (posts) {
-                       Post post = posts.get(postId);
-                       if ((post == null) && create) {
-                               post = new PostImpl(postId);
-                               posts.put(postId, post);
-                       }
-                       return post;
-               }
+       public Collection<Post> getPosts(String soneId) {
+               return database.getPosts(soneId);
        }
 
        /**
-        * Returns all posts that have the given Sone as recipient.
-        *
-        * @see Post#getRecipient()
-        * @param recipient
-        *            The recipient of the posts
-        * @return All posts that have the given Sone as recipient
-        */
-       public Set<Post> getDirectedPosts(Sone recipient) {
-               Validation.begin().isNotNull("Recipient", recipient).check();
-               Set<Post> directedPosts = new HashSet<Post>();
-               synchronized (posts) {
-                       for (Post post : posts.values()) {
-                               if (recipient.equals(post.getRecipient())) {
-                                       directedPosts.add(post);
-                               }
-                       }
-               }
-               return directedPosts;
+        * {@inheritDoc}
+        */
+       @Override
+       public Collection<Post> getDirectedPosts(final String recipientId) {
+               checkNotNull(recipientId, "recipient must not be null");
+               return database.getDirectedPosts(recipientId);
        }
 
        /**
-        * Returns the reply with the given ID. If there is no reply with the given
-        * ID yet, a new one is created.
+        * Returns a post reply builder.
         *
-        * @param replyId
-        *            The ID of the reply to get
-        * @return The reply
+        * @return A new post reply builder
         */
-       public PostReply getReply(String replyId) {
-               return getReply(replyId, true);
+       public PostReplyBuilder postReplyBuilder() {
+               return database.newPostReplyBuilder();
        }
 
        /**
-        * Returns the reply with the given ID. If there is no reply with the given
-        * ID yet, a new one is created, unless {@code create} is false in which
-        * case {@code null} is returned.
-        *
-        * @param replyId
-        *            The ID of the reply to get
-        * @param create
-        *            {@code true} to always return a {@link Reply}, {@code false}
-        *            to return {@code null} if no reply can be found
-        * @return The reply, or {@code null} if there is no such reply
-        */
-       public PostReply getReply(String replyId, boolean create) {
-               synchronized (replies) {
-                       PostReply reply = replies.get(replyId);
-                       if (create && (reply == null)) {
-                               reply = new PostReply(replyId);
-                               replies.put(replyId, reply);
-                       }
-                       return reply;
-               }
+        * {@inheritDoc}
+        */
+       @Override
+       public Optional<PostReply> getPostReply(String replyId) {
+               return database.getPostReply(replyId);
        }
 
        /**
-        * Returns all replies for the given post, order ascending by time.
-        *
-        * @param post
-        *            The post to get all replies for
-        * @return All replies for the given post
+        * {@inheritDoc}
         */
-       public List<PostReply> getReplies(Post post) {
-               Set<Sone> sones = getSones();
-               List<PostReply> replies = new ArrayList<PostReply>();
-               for (Sone sone : sones) {
-                       for (PostReply reply : sone.getReplies()) {
-                               if (reply.getPost().equals(post)) {
-                                       replies.add(reply);
-                               }
-                       }
-               }
-               Collections.sort(replies, Reply.TIME_COMPARATOR);
-               return replies;
+       @Override
+       public List<PostReply> getReplies(final String postId) {
+               return database.getReplies(postId);
        }
 
        /**
@@ -706,21 +502,7 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
         *         otherwise
         */
        public boolean isBookmarked(Post post) {
-               return isPostBookmarked(post.getId());
-       }
-
-       /**
-        * Returns whether the post with the given ID is bookmarked.
-        *
-        * @param id
-        *            The ID of the post to check
-        * @return {@code true} if the post with the given ID is bookmarked,
-        *         {@code false} otherwise
-        */
-       public boolean isPostBookmarked(String id) {
-               synchronized (bookmarkedPosts) {
-                       return bookmarkedPosts.contains(id);
-               }
+               return database.isPostBookmarked(post);
        }
 
        /**
@@ -729,28 +511,11 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
         * @return All bookmarked posts
         */
        public Set<Post> getBookmarkedPosts() {
-               Set<Post> posts = new HashSet<Post>();
-               synchronized (bookmarkedPosts) {
-                       for (String bookmarkedPostId : bookmarkedPosts) {
-                               Post post = getPost(bookmarkedPostId, false);
-                               if (post != null) {
-                                       posts.add(post);
-                               }
-                       }
-               }
-               return posts;
+               return database.getBookmarkedPosts();
        }
 
-       /**
-        * Returns the album with the given ID, creating a new album if no album
-        * with the given ID can be found.
-        *
-        * @param albumId
-        *            The ID of the album
-        * @return The album with the given ID
-        */
-       public Album getAlbum(String albumId) {
-               return getAlbum(albumId, true);
+       public AlbumBuilder albumBuilder() {
+               return database.newAlbumBuilder();
        }
 
        /**
@@ -759,21 +524,15 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
         *
         * @param albumId
         *            The ID of the album
-        * @param create
-        *            {@code true} to create a new album if none exists for the
-        *            given ID
         * @return The album with the given ID, or {@code null} if no album with the
-        *         given ID exists and {@code create} is {@code false}
-        */
-       public Album getAlbum(String albumId, boolean create) {
-               synchronized (albums) {
-                       Album album = albums.get(albumId);
-                       if (create && (album == null)) {
-                               album = new Album(albumId);
-                               albums.put(albumId, album);
-                       }
-                       return album;
-               }
+        *         given ID exists
+        */
+       public Album getAlbum(String albumId) {
+               return database.getAlbum(albumId).orNull();
+       }
+
+       public ImageBuilder imageBuilder() {
+               return database.newImageBuilder();
        }
 
        /**
@@ -800,14 +559,16 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
         *         none was created
         */
        public Image getImage(String imageId, boolean create) {
-               synchronized (images) {
-                       Image image = images.get(imageId);
-                       if (create && (image == null)) {
-                               image = new Image(imageId);
-                               images.put(imageId, image);
-                       }
-                       return image;
+               Optional<Image> image = database.getImage(imageId);
+               if (image.isPresent()) {
+                       return image.get();
+               }
+               if (!create) {
+                       return null;
                }
+               Image newImage = database.newImageBuilder().withId(imageId).build();
+               database.storeImage(newImage);
+               return newImage;
        }
 
        /**
@@ -839,7 +600,7 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
        public void lockSone(Sone sone) {
                synchronized (lockedSones) {
                        if (lockedSones.add(sone)) {
-                               coreListenerManager.fireSoneLocked(sone);
+                               eventBus.post(new SoneLockedEvent(sone));
                        }
                }
        }
@@ -854,7 +615,7 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
        public void unlockSone(Sone sone) {
                synchronized (lockedSones) {
                        if (lockedSones.remove(sone)) {
-                               coreListenerManager.fireSoneUnlocked(sone);
+                               eventBus.post(new SoneUnlockedEvent(sone));
                        }
                }
        }
@@ -871,27 +632,20 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
                        logger.log(Level.WARNING, "Given OwnIdentity is null!");
                        return null;
                }
-               synchronized (localSones) {
-                       final Sone sone;
-                       try {
-                               sone = getLocalSone(ownIdentity.getId()).setIdentity(ownIdentity).setInsertUri(new FreenetURI(ownIdentity.getInsertUri())).setRequestUri(new FreenetURI(ownIdentity.getRequestUri()));
-                       } catch (MalformedURLException mue1) {
-                               logger.log(Level.SEVERE, String.format("Could not convert the Identity’s URIs to Freenet URIs: %s, %s", ownIdentity.getInsertUri(), ownIdentity.getRequestUri()), mue1);
-                               return null;
-                       }
-                       sone.setLatestEdition(Numbers.safeParseLong(ownIdentity.getProperty("Sone.LatestEdition"), (long) 0));
-                       sone.setClient(new Client("Sone", SonePlugin.VERSION.toString()));
-                       sone.setKnown(true);
-                       /* TODO - load posts ’n stuff */
-                       localSones.put(ownIdentity.getId(), sone);
-                       final SoneInserter soneInserter = new SoneInserter(this, freenetInterface, sone);
-                       soneInserter.addSoneInsertListener(this);
+               logger.info(String.format("Adding Sone from OwnIdentity: %s", ownIdentity));
+               Sone sone = database.newSoneBuilder().local().from(ownIdentity).build();
+               sone.setLatestEdition(fromNullable(tryParse(ownIdentity.getProperty("Sone.LatestEdition"))).or(0L));
+               sone.setClient(new Client("Sone", SonePlugin.VERSION.toString()));
+               sone.setKnown(true);
+               SoneInserter soneInserter = new SoneInserter(this, eventBus, freenetInterface, ownIdentity.getId());
+               eventBus.register(soneInserter);
+               synchronized (soneInserters) {
                        soneInserters.put(sone, soneInserter);
-                       sone.setStatus(SoneStatus.idle);
-                       loadSone(sone);
-                       soneInserter.start();
-                       return sone;
                }
+               loadSone(sone);
+               sone.setStatus(SoneStatus.idle);
+               soneInserter.start();
+               return sone;
        }
 
        /**
@@ -907,14 +661,8 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
                        return null;
                }
                Sone sone = addLocalSone(ownIdentity);
-               sone.getOptions().addBooleanOption("AutoFollow", new DefaultOption<Boolean>(false));
-               sone.getOptions().addBooleanOption("EnableSoneInsertNotifications", new DefaultOption<Boolean>(false));
-               sone.getOptions().addBooleanOption("ShowNotification/NewSones", new DefaultOption<Boolean>(true));
-               sone.getOptions().addBooleanOption("ShowNotification/NewPosts", new DefaultOption<Boolean>(true));
-               sone.getOptions().addBooleanOption("ShowNotification/NewReplies", new DefaultOption<Boolean>(true));
-               sone.getOptions().addEnumOption("ShowCustomAvatars", new DefaultOption<ShowCustomAvatars>(ShowCustomAvatars.NEVER));
-
-               followSone(sone, getSone("nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI"));
+
+               followSone(sone, "nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI");
                touchConfiguration();
                return sone;
        }
@@ -931,37 +679,33 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
                        logger.log(Level.WARNING, "Given Identity is null!");
                        return null;
                }
-               synchronized (remoteSones) {
-                       final Sone sone = getRemoteSone(identity.getId(), true).setIdentity(identity);
-                       boolean newSone = sone.getRequestUri() == null;
-                       sone.setRequestUri(getSoneUri(identity.getRequestUri()));
-                       sone.setLatestEdition(Numbers.safeParseLong(identity.getProperty("Sone.LatestEdition"), (long) 0));
+               final Long latestEdition = tryParse(fromNullable(
+                               identity.getProperty("Sone.LatestEdition")).or("0"));
+               Optional<Sone> existingSone = getSone(identity.getId());
+               if (existingSone.isPresent() && existingSone.get().isLocal()) {
+                       return existingSone.get();
+               }
+               boolean newSone = !existingSone.isPresent();
+               Sone sone = !newSone ? existingSone.get() : database.newSoneBuilder().from(identity).build();
+               sone.setLatestEdition(latestEdition);
+               if (newSone) {
+                       synchronized (knownSones) {
+                               newSone = !knownSones.contains(sone.getId());
+                       }
+                       sone.setKnown(!newSone);
                        if (newSone) {
-                               synchronized (knownSones) {
-                                       newSone = !knownSones.contains(sone.getId());
-                               }
-                               sone.setKnown(!newSone);
-                               if (newSone) {
-                                       coreListenerManager.fireNewSoneFound(sone);
-                                       for (Sone localSone : getLocalSones()) {
-                                               if (localSone.getOptions().getBooleanOption("AutoFollow").get()) {
-                                                       followSone(localSone, sone);
-                                               }
+                               eventBus.post(new NewSoneFoundEvent(sone));
+                               for (Sone localSone : getLocalSones()) {
+                                       if (localSone.getOptions().isAutoFollow()) {
+                                               followSone(localSone, sone.getId());
                                        }
                                }
                        }
-                       soneDownloader.addSone(sone);
-                       soneDownloaders.execute(new Runnable() {
-
-                               @Override
-                               @SuppressWarnings("synthetic-access")
-                               public void run() {
-                                       soneDownloader.fetchSone(sone, sone.getRequestUri());
-                               }
-
-                       });
-                       return sone;
                }
+               database.storeSone(sone);
+               soneDownloader.addSone(sone);
+               soneDownloaders.execute(soneDownloader.fetchSoneWithUriAction(sone));
+               return sone;
        }
 
        /**
@@ -973,39 +717,23 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
         *            The ID of the Sone to follow
         */
        public void followSone(Sone sone, String soneId) {
-               Validation.begin().isNotNull("Sone", sone).isNotNull("Sone ID", soneId).check();
-               Sone followedSone = getSone(soneId, true);
-               if (followedSone == null) {
-                       logger.log(Level.INFO, String.format("Ignored Sone with invalid ID: %s", soneId));
-                       return;
-               }
-               followSone(sone, getSone(soneId));
-       }
-
-       /**
-        * Lets the given local Sone follow the other given Sone. If the given Sone
-        * was not followed by any local Sone before, this will mark all elements of
-        * the followed Sone as read that have been created before the current
-        * moment.
-        *
-        * @param sone
-        *            The local Sone that should follow the other Sone
-        * @param followedSone
-        *            The Sone that should be followed
-        */
-       public void followSone(Sone sone, Sone followedSone) {
-               Validation.begin().isNotNull("Sone", sone).isNotNull("Followed Sone", followedSone).check();
-               sone.addFriend(followedSone.getId());
+               checkNotNull(sone, "sone must not be null");
+               checkNotNull(soneId, "soneId must not be null");
+               sone.addFriend(soneId);
                synchronized (soneFollowingTimes) {
-                       if (!soneFollowingTimes.containsKey(followedSone)) {
+                       if (!soneFollowingTimes.containsKey(soneId)) {
                                long now = System.currentTimeMillis();
-                               soneFollowingTimes.put(followedSone, now);
-                               for (Post post : followedSone.getPosts()) {
+                               soneFollowingTimes.put(soneId, now);
+                               Optional<Sone> followedSone = getSone(soneId);
+                               if (!followedSone.isPresent()) {
+                                       return;
+                               }
+                               for (Post post : followedSone.get().getPosts()) {
                                        if (post.getTime() < now) {
                                                markPostKnown(post);
                                        }
                                }
-                               for (PostReply reply : followedSone.getReplies()) {
+                               for (PostReply reply : followedSone.get().getReplies()) {
                                        if (reply.getTime() < now) {
                                                markReplyKnown(reply);
                                        }
@@ -1024,30 +752,16 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
         *            The ID of the Sone being unfollowed
         */
        public void unfollowSone(Sone sone, String soneId) {
-               Validation.begin().isNotNull("Sone", sone).isNotNull("Sone ID", soneId).check();
-               unfollowSone(sone, getSone(soneId, false));
-       }
-
-       /**
-        * Lets the given local Sone unfollow the other given Sone. If the given
-        * local Sone is the last local Sone that followed the given Sone, its
-        * following time will be removed.
-        *
-        * @param sone
-        *            The local Sone that should unfollow another Sone
-        * @param unfollowedSone
-        *            The Sone being unfollowed
-        */
-       public void unfollowSone(Sone sone, Sone unfollowedSone) {
-               Validation.begin().isNotNull("Sone", sone).isNotNull("Unfollowed Sone", unfollowedSone).check();
-               sone.removeFriend(unfollowedSone.getId());
+               checkNotNull(sone, "sone must not be null");
+               checkNotNull(soneId, "soneId must not be null");
+               sone.removeFriend(soneId);
                boolean unfollowedSoneStillFollowed = false;
                for (Sone localSone : getLocalSones()) {
-                       unfollowedSoneStillFollowed |= localSone.hasFriend(unfollowedSone.getId());
+                       unfollowedSoneStillFollowed |= localSone.hasFriend(soneId);
                }
                if (!unfollowedSoneStillFollowed) {
                        synchronized (soneFollowingTimes) {
-                               soneFollowingTimes.remove(unfollowedSone);
+                               soneFollowingTimes.remove(soneId);
                        }
                }
                touchConfiguration();
@@ -1064,7 +778,10 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
         *            The trust value (from {@code -100} to {@code 100})
         */
        public void setTrust(Sone origin, Sone target, int trustValue) {
-               Validation.begin().isNotNull("Trust Origin", origin).check().isInstanceOf("Trust Origin", origin.getIdentity(), OwnIdentity.class).isNotNull("Trust Target", target).isLessOrEqual("Trust Value", trustValue, 100).isGreaterOrEqual("Trust Value", trustValue, -100).check();
+               checkNotNull(origin, "origin must not be null");
+               checkArgument(origin.getIdentity() instanceof OwnIdentity, "origin must be a local Sone");
+               checkNotNull(target, "target must not be null");
+               checkArgument((trustValue >= -100) && (trustValue <= 100), "trustValue must be within [-100, 100]");
                webOfTrustUpdater.setTrust((OwnIdentity) origin.getIdentity(), target.getIdentity(), trustValue, preferences.getTrustComment());
        }
 
@@ -1077,7 +794,9 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
         *            The trust target
         */
        public void removeTrust(Sone origin, Sone target) {
-               Validation.begin().isNotNull("Trust Origin", origin).isNotNull("Trust Target", target).check().isInstanceOf("Trust Origin Identity", origin.getIdentity(), OwnIdentity.class).check();
+               checkNotNull(origin, "origin must not be null");
+               checkNotNull(target, "target must not be null");
+               checkArgument(origin.getIdentity() instanceof OwnIdentity, "origin must be a local Sone");
                webOfTrustUpdater.setTrust((OwnIdentity) origin.getIdentity(), target.getIdentity(), null, null);
        }
 
@@ -1138,117 +857,62 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
         *            {@code true} if the stored Sone should be updated regardless
         *            of the age of the given Sone
         */
-       public void updateSone(Sone sone, boolean soneRescueMode) {
-               if (hasSone(sone.getId())) {
-                       Sone storedSone = getSone(sone.getId());
-                       if (!soneRescueMode && !(sone.getTime() > storedSone.getTime())) {
+       public void updateSone(final Sone sone, boolean soneRescueMode) {
+               Optional<Sone> storedSone = getSone(sone.getId());
+               if (storedSone.isPresent()) {
+                       if (!soneRescueMode && !(sone.getTime() > storedSone.get().getTime())) {
                                logger.log(Level.FINE, String.format("Downloaded Sone %s is not newer than stored Sone %s.", sone, storedSone));
                                return;
                        }
-                       synchronized (posts) {
-                               if (!soneRescueMode) {
-                                       for (Post post : storedSone.getPosts()) {
-                                               posts.remove(post.getId());
-                                               if (!sone.getPosts().contains(post)) {
-                                                       coreListenerManager.firePostRemoved(post);
-                                               }
-                                       }
-                               }
-                               List<Post> storedPosts = storedSone.getPosts();
-                               synchronized (knownPosts) {
-                                       for (Post post : sone.getPosts()) {
-                                               post.setSone(storedSone).setKnown(knownPosts.contains(post.getId()));
-                                               if (!storedPosts.contains(post)) {
-                                                       if (post.getTime() < getSoneFollowingTime(sone)) {
-                                                               knownPosts.add(post.getId());
-                                                               post.setKnown(true);
-                                                       } else if (!knownPosts.contains(post.getId())) {
-                                                               coreListenerManager.fireNewPostFound(post);
-                                                       }
-                                               }
-                                               posts.put(post.getId(), post);
-                                       }
-                               }
-                       }
-                       synchronized (replies) {
-                               if (!soneRescueMode) {
-                                       for (PostReply reply : storedSone.getReplies()) {
-                                               replies.remove(reply.getId());
-                                               if (!sone.getReplies().contains(reply)) {
-                                                       coreListenerManager.fireReplyRemoved(reply);
-                                               }
+                       /* find removed posts. */
+                       SoneChangeDetector soneChangeDetector = new SoneChangeDetector(storedSone.get());
+                       soneChangeDetector.onNewPosts(new PostProcessor() {
+                               @Override
+                               public void processPost(Post post) {
+                                       if (post.getTime() < getSoneFollowingTime(sone)) {
+                                               post.setKnown(true);
+                                       } else if (!post.isKnown()) {
+                                               eventBus.post(new NewPostFoundEvent(post));
                                        }
                                }
-                               Set<PostReply> storedReplies = storedSone.getReplies();
-                               synchronized (knownReplies) {
-                                       for (PostReply reply : sone.getReplies()) {
-                                               reply.setSone(storedSone).setKnown(knownReplies.contains(reply.getId()));
-                                               if (!storedReplies.contains(reply)) {
-                                                       if (reply.getTime() < getSoneFollowingTime(sone)) {
-                                                               knownReplies.add(reply.getId());
-                                                               reply.setKnown(true);
-                                                       } else if (!knownReplies.contains(reply.getId())) {
-                                                               coreListenerManager.fireNewReplyFound(reply);
-                                                       }
-                                               }
-                                               replies.put(reply.getId(), reply);
-                                       }
+                       });
+                       soneChangeDetector.onRemovedPosts(new PostProcessor() {
+                               @Override
+                               public void processPost(Post post) {
+                                       eventBus.post(new PostRemovedEvent(post));
                                }
-                       }
-                       synchronized (albums) {
-                               synchronized (images) {
-                                       for (Album album : storedSone.getAlbums()) {
-                                               albums.remove(album.getId());
-                                               for (Image image : album.getImages()) {
-                                                       images.remove(image.getId());
-                                               }
-                                       }
-                                       for (Album album : sone.getAlbums()) {
-                                               albums.put(album.getId(), album);
-                                               for (Image image : album.getImages()) {
-                                                       images.put(image.getId(), image);
-                                               }
+                       });
+                       soneChangeDetector.onNewPostReplies(new PostReplyProcessor() {
+                               @Override
+                               public void processPostReply(PostReply postReply) {
+                                       if (postReply.getTime() < getSoneFollowingTime(sone)) {
+                                               postReply.setKnown(true);
+                                       } else if (!postReply.isKnown()) {
+                                               eventBus.post(new NewPostReplyFoundEvent(postReply));
                                        }
                                }
-                       }
-                       synchronized (storedSone) {
-                               if (!soneRescueMode || (sone.getTime() > storedSone.getTime())) {
-                                       storedSone.setTime(sone.getTime());
-                               }
-                               storedSone.setClient(sone.getClient());
-                               storedSone.setProfile(sone.getProfile());
-                               if (soneRescueMode) {
-                                       for (Post post : sone.getPosts()) {
-                                               storedSone.addPost(post);
-                                       }
-                                       for (PostReply reply : sone.getReplies()) {
-                                               storedSone.addReply(reply);
-                                       }
-                                       for (String likedPostId : sone.getLikedPostIds()) {
-                                               storedSone.addLikedPostId(likedPostId);
-                                       }
-                                       for (String likedReplyId : sone.getLikedReplyIds()) {
-                                               storedSone.addLikedReplyId(likedReplyId);
-                                       }
-                                       for (Album album : sone.getAlbums()) {
-                                               storedSone.addAlbum(album);
-                                       }
-                               } else {
-                                       storedSone.setPosts(sone.getPosts());
-                                       storedSone.setReplies(sone.getReplies());
-                                       storedSone.setLikePostIds(sone.getLikedPostIds());
-                                       storedSone.setLikeReplyIds(sone.getLikedReplyIds());
-                                       storedSone.setAlbums(sone.getAlbums());
+                       });
+                       soneChangeDetector.onRemovedPostReplies(new PostReplyProcessor() {
+                               @Override
+                               public void processPostReply(PostReply postReply) {
+                                       eventBus.post(new PostReplyRemovedEvent(postReply));
                                }
-                               storedSone.setLatestEdition(sone.getLatestEdition());
+                       });
+                       soneChangeDetector.detectChanges(sone);
+                       database.storeSone(sone);
+                       sone.setOptions(storedSone.get().getOptions());
+                       sone.setKnown(storedSone.get().isKnown());
+                       sone.setStatus((sone.getTime() == 0) ? SoneStatus.unknown : SoneStatus.idle);
+                       if (sone.isLocal()) {
+                               touchConfiguration();
                        }
                }
        }
 
        /**
         * Deletes the given Sone. This will remove the Sone from the
-        * {@link #getLocalSone(String) local Sones}, stops its {@link SoneInserter}
-        * and remove the context from its identity.
+        * {@link #getLocalSones() local Sones}, stop its {@link SoneInserter} and
+        * remove the context from its identity.
         *
         * @param sone
         *            The Sone to delete
@@ -1258,16 +922,13 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
                        logger.log(Level.WARNING, String.format("Tried to delete Sone of non-own identity: %s", sone));
                        return;
                }
-               synchronized (localSones) {
-                       if (!localSones.containsKey(sone.getId())) {
-                               logger.log(Level.WARNING, String.format("Tried to delete non-local Sone: %s", sone));
-                               return;
-                       }
-                       localSones.remove(sone.getId());
-                       SoneInserter soneInserter = soneInserters.remove(sone);
-                       soneInserter.removeSoneInsertListener(this);
-                       soneInserter.stop();
+               if (!getLocalSones().contains(sone)) {
+                       logger.log(Level.WARNING, String.format("Tried to delete non-local Sone: %s", sone));
+                       return;
                }
+               SoneInserter soneInserter = soneInserters.remove(sone);
+               soneInserter.stop();
+               database.removeSone(sone);
                webOfTrustUpdater.removeContext((OwnIdentity) sone.getIdentity(), "Sone");
                webOfTrustUpdater.removeProperty((OwnIdentity) sone.getIdentity(), "Sone.LatestEdition");
                try {
@@ -1279,7 +940,7 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
 
        /**
         * Marks the given Sone as known. If the Sone was not {@link Post#isKnown()
-        * known} before, a {@link CoreListener#markSoneKnown(Sone)} event is fired.
+        * known} before, a {@link MarkSoneKnownEvent} is fired.
         *
         * @param sone
         *            The Sone to mark as known
@@ -1290,7 +951,7 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
                        synchronized (knownSones) {
                                knownSones.add(sone.getId());
                        }
-                       coreListenerManager.fireMarkSoneKnown(sone);
+                       eventBus.post(new MarkSoneKnownEvent(sone));
                        touchConfiguration();
                }
        }
@@ -1303,18 +964,11 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
         *            The Sone to load and update
         */
        public void loadSone(Sone sone) {
-               if (!isLocalSone(sone)) {
+               if (!sone.isLocal()) {
                        logger.log(Level.FINE, String.format("Tried to load non-local Sone: %s", sone));
                        return;
                }
-
-               /* initialize options. */
-               sone.getOptions().addBooleanOption("AutoFollow", new DefaultOption<Boolean>(false));
-               sone.getOptions().addBooleanOption("EnableSoneInsertNotifications", new DefaultOption<Boolean>(false));
-               sone.getOptions().addBooleanOption("ShowNotification/NewSones", new DefaultOption<Boolean>(true));
-               sone.getOptions().addBooleanOption("ShowNotification/NewPosts", new DefaultOption<Boolean>(true));
-               sone.getOptions().addBooleanOption("ShowNotification/NewReplies", new DefaultOption<Boolean>(true));
-               sone.getOptions().addEnumOption("ShowCustomAvatars", new DefaultOption<ShowCustomAvatars>(ShowCustomAvatars.NEVER));
+               logger.info(String.format("Loading local Sone: %s", sone));
 
                /* load Sone. */
                String sonePrefix = "Sone/" + sone.getId();
@@ -1326,169 +980,80 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
                String lastInsertFingerprint = configuration.getStringValue(sonePrefix + "/LastInsertFingerprint").getValue("");
 
                /* load profile. */
-               Profile profile = new Profile(sone);
-               profile.setFirstName(configuration.getStringValue(sonePrefix + "/Profile/FirstName").getValue(null));
-               profile.setMiddleName(configuration.getStringValue(sonePrefix + "/Profile/MiddleName").getValue(null));
-               profile.setLastName(configuration.getStringValue(sonePrefix + "/Profile/LastName").getValue(null));
-               profile.setBirthDay(configuration.getIntValue(sonePrefix + "/Profile/BirthDay").getValue(null));
-               profile.setBirthMonth(configuration.getIntValue(sonePrefix + "/Profile/BirthMonth").getValue(null));
-               profile.setBirthYear(configuration.getIntValue(sonePrefix + "/Profile/BirthYear").getValue(null));
-
-               /* load profile fields. */
-               while (true) {
-                       String fieldPrefix = sonePrefix + "/Profile/Fields/" + profile.getFields().size();
-                       String fieldName = configuration.getStringValue(fieldPrefix + "/Name").getValue(null);
-                       if (fieldName == null) {
-                               break;
-                       }
-                       String fieldValue = configuration.getStringValue(fieldPrefix + "/Value").getValue("");
-                       profile.addField(fieldName).setValue(fieldValue);
-               }
+               ConfigurationSoneParser configurationSoneParser = new ConfigurationSoneParser(configuration, sone);
+               Profile profile = configurationSoneParser.parseProfile();
 
                /* load posts. */
-               Set<Post> posts = new HashSet<Post>();
-               while (true) {
-                       String postPrefix = sonePrefix + "/Posts/" + posts.size();
-                       String postId = configuration.getStringValue(postPrefix + "/ID").getValue(null);
-                       if (postId == null) {
-                               break;
-                       }
-                       String postRecipientId = configuration.getStringValue(postPrefix + "/Recipient").getValue(null);
-                       long postTime = configuration.getLongValue(postPrefix + "/Time").getValue((long) 0);
-                       String postText = configuration.getStringValue(postPrefix + "/Text").getValue(null);
-                       if ((postTime == 0) || (postText == null)) {
-                               logger.log(Level.WARNING, "Invalid post found, aborting load!");
-                               return;
-                       }
-                       Post post = getPost(postId).setSone(sone).setTime(postTime).setText(postText);
-                       if ((postRecipientId != null) && (postRecipientId.length() == 43)) {
-                               post.setRecipient(getSone(postRecipientId));
-                       }
-                       posts.add(post);
+               Collection<Post> posts;
+               try {
+                       posts = configurationSoneParser.parsePosts(database);
+               } catch (InvalidPostFound ipf) {
+                       logger.log(Level.WARNING, "Invalid post found, aborting load!");
+                       return;
                }
 
                /* load replies. */
-               Set<PostReply> replies = new HashSet<PostReply>();
-               while (true) {
-                       String replyPrefix = sonePrefix + "/Replies/" + replies.size();
-                       String replyId = configuration.getStringValue(replyPrefix + "/ID").getValue(null);
-                       if (replyId == null) {
-                               break;
-                       }
-                       String postId = configuration.getStringValue(replyPrefix + "/Post/ID").getValue(null);
-                       long replyTime = configuration.getLongValue(replyPrefix + "/Time").getValue((long) 0);
-                       String replyText = configuration.getStringValue(replyPrefix + "/Text").getValue(null);
-                       if ((postId == null) || (replyTime == 0) || (replyText == null)) {
-                               logger.log(Level.WARNING, "Invalid reply found, aborting load!");
-                               return;
-                       }
-                       replies.add(getReply(replyId).setSone(sone).setPost(getPost(postId)).setTime(replyTime).setText(replyText));
+               Collection<PostReply> replies;
+               try {
+                       replies = configurationSoneParser.parsePostReplies(database);
+               } catch (InvalidPostReplyFound iprf) {
+                       logger.log(Level.WARNING, "Invalid reply found, aborting load!");
+                       return;
                }
 
                /* load post likes. */
-               Set<String> likedPostIds = new HashSet<String>();
-               while (true) {
-                       String likedPostId = configuration.getStringValue(sonePrefix + "/Likes/Post/" + likedPostIds.size() + "/ID").getValue(null);
-                       if (likedPostId == null) {
-                               break;
-                       }
-                       likedPostIds.add(likedPostId);
-               }
+               Set<String> likedPostIds =
+                               configurationSoneParser.parseLikedPostIds();
 
                /* load reply likes. */
-               Set<String> likedReplyIds = new HashSet<String>();
-               while (true) {
-                       String likedReplyId = configuration.getStringValue(sonePrefix + "/Likes/Reply/" + likedReplyIds.size() + "/ID").getValue(null);
-                       if (likedReplyId == null) {
-                               break;
-                       }
-                       likedReplyIds.add(likedReplyId);
-               }
+               Set<String> likedReplyIds =
+                               configurationSoneParser.parseLikedPostReplyIds();
 
                /* load friends. */
-               Set<String> friends = new HashSet<String>();
-               while (true) {
-                       String friendId = configuration.getStringValue(sonePrefix + "/Friends/" + friends.size() + "/ID").getValue(null);
-                       if (friendId == null) {
-                               break;
-                       }
-                       friends.add(friendId);
-               }
+               Set<String> friends = configurationSoneParser.parseFriends();
 
                /* load albums. */
-               List<Album> topLevelAlbums = new ArrayList<Album>();
-               int albumCounter = 0;
-               while (true) {
-                       String albumPrefix = sonePrefix + "/Albums/" + albumCounter++;
-                       String albumId = configuration.getStringValue(albumPrefix + "/ID").getValue(null);
-                       if (albumId == null) {
-                               break;
-                       }
-                       String albumTitle = configuration.getStringValue(albumPrefix + "/Title").getValue(null);
-                       String albumDescription = configuration.getStringValue(albumPrefix + "/Description").getValue(null);
-                       String albumParentId = configuration.getStringValue(albumPrefix + "/Parent").getValue(null);
-                       String albumImageId = configuration.getStringValue(albumPrefix + "/AlbumImage").getValue(null);
-                       if ((albumTitle == null) || (albumDescription == null)) {
-                               logger.log(Level.WARNING, "Invalid album found, aborting load!");
-                               return;
-                       }
-                       Album album = getAlbum(albumId).setSone(sone).setTitle(albumTitle).setDescription(albumDescription).setAlbumImage(albumImageId);
-                       if (albumParentId != null) {
-                               Album parentAlbum = getAlbum(albumParentId, false);
-                               if (parentAlbum == null) {
-                                       logger.log(Level.WARNING, String.format("Invalid parent album ID: %s", albumParentId));
-                                       return;
-                               }
-                               parentAlbum.addAlbum(album);
-                       } else {
-                               if (!topLevelAlbums.contains(album)) {
-                                       topLevelAlbums.add(album);
-                               }
-                       }
+               List<Album> topLevelAlbums;
+               try {
+                       topLevelAlbums =
+                                       configurationSoneParser.parseTopLevelAlbums(database);
+               } catch (InvalidAlbumFound iaf) {
+                       logger.log(Level.WARNING, "Invalid album found, aborting load!");
+                       return;
+               } catch (InvalidParentAlbumFound ipaf) {
+                       logger.log(Level.WARNING, format("Invalid parent album ID: %s",
+                                       ipaf.getAlbumParentId()));
+                       return;
                }
 
                /* load images. */
-               int imageCounter = 0;
-               while (true) {
-                       String imagePrefix = sonePrefix + "/Images/" + imageCounter++;
-                       String imageId = configuration.getStringValue(imagePrefix + "/ID").getValue(null);
-                       if (imageId == null) {
-                               break;
-                       }
-                       String albumId = configuration.getStringValue(imagePrefix + "/Album").getValue(null);
-                       String key = configuration.getStringValue(imagePrefix + "/Key").getValue(null);
-                       String title = configuration.getStringValue(imagePrefix + "/Title").getValue(null);
-                       String description = configuration.getStringValue(imagePrefix + "/Description").getValue(null);
-                       Long creationTime = configuration.getLongValue(imagePrefix + "/CreationTime").getValue(null);
-                       Integer width = configuration.getIntValue(imagePrefix + "/Width").getValue(null);
-                       Integer height = configuration.getIntValue(imagePrefix + "/Height").getValue(null);
-                       if ((albumId == null) || (key == null) || (title == null) || (description == null) || (creationTime == null) || (width == null) || (height == null)) {
-                               logger.log(Level.WARNING, "Invalid image found, aborting load!");
-                               return;
-                       }
-                       Album album = getAlbum(albumId, false);
-                       if (album == null) {
-                               logger.log(Level.WARNING, "Invalid album image encountered, aborting load!");
-                               return;
-                       }
-                       Image image = getImage(imageId).setSone(sone).setCreationTime(creationTime).setKey(key);
-                       image.setTitle(title).setDescription(description).setWidth(width).setHeight(height);
-                       album.addImage(image);
+               try {
+                       configurationSoneParser.parseImages(database);
+               } catch (InvalidImageFound iif) {
+                       logger.log(WARNING, "Invalid image found, aborting load!");
+                       return;
+               } catch (InvalidParentAlbumFound ipaf) {
+                       logger.log(Level.WARNING,
+                                       format("Invalid album image (%s) encountered, aborting load!",
+                                                       ipaf.getAlbumParentId()));
+                       return;
                }
 
                /* load avatar. */
                String avatarId = configuration.getStringValue(sonePrefix + "/Profile/Avatar").getValue(null);
                if (avatarId != null) {
-                       profile.setAvatar(getImage(avatarId, false));
+                       final Map<String, Image> images =
+                                       configurationSoneParser.getImages();
+                       profile.setAvatar(images.get(avatarId));
                }
 
                /* load options. */
-               sone.getOptions().getBooleanOption("AutoFollow").set(configuration.getBooleanValue(sonePrefix + "/Options/AutoFollow").getValue(null));
-               sone.getOptions().getBooleanOption("EnableSoneInsertNotifications").set(configuration.getBooleanValue(sonePrefix + "/Options/EnableSoneInsertNotifications").getValue(null));
-               sone.getOptions().getBooleanOption("ShowNotification/NewSones").set(configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewSones").getValue(null));
-               sone.getOptions().getBooleanOption("ShowNotification/NewPosts").set(configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewPosts").getValue(null));
-               sone.getOptions().getBooleanOption("ShowNotification/NewReplies").set(configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewReplies").getValue(null));
-               sone.getOptions().<ShowCustomAvatars> getEnumOption("ShowCustomAvatars").set(ShowCustomAvatars.valueOf(configuration.getStringValue(sonePrefix + "/Options/ShowCustomAvatars").getValue(ShowCustomAvatars.NEVER.name())));
+               sone.getOptions().setAutoFollow(configuration.getBooleanValue(sonePrefix + "/Options/AutoFollow").getValue(null));
+               sone.getOptions().setSoneInsertNotificationEnabled(configuration.getBooleanValue(sonePrefix + "/Options/EnableSoneInsertNotifications").getValue(null));
+               sone.getOptions().setShowNewSoneNotifications(configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewSones").getValue(null));
+               sone.getOptions().setShowNewPostNotifications(configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewPosts").getValue(null));
+               sone.getOptions().setShowNewReplyNotifications(configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewReplies").getValue(null));
+               sone.getOptions().setShowCustomAvatars(ShowCustomAvatars.valueOf(configuration.getStringValue(sonePrefix + "/Options/ShowCustomAvatars").getValue(ShowCustomAvatars.NEVER.name())));
 
                /* if we’re still here, Sone was loaded successfully. */
                synchronized (sone) {
@@ -1501,52 +1066,30 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
                        for (String friendId : friends) {
                                followSone(sone, friendId);
                        }
-                       sone.setAlbums(topLevelAlbums);
-                       soneInserters.get(sone).setLastInsertFingerprint(lastInsertFingerprint);
+                       for (Album album : sone.getRootAlbum().getAlbums()) {
+                               sone.getRootAlbum().removeAlbum(album);
+                       }
+                       for (Album album : topLevelAlbums) {
+                               sone.getRootAlbum().addAlbum(album);
+                       }
+                       database.storeSone(sone);
+                       synchronized (soneInserters) {
+                               soneInserters.get(sone).setLastInsertFingerprint(lastInsertFingerprint);
+                       }
                }
                synchronized (knownSones) {
                        for (String friend : friends) {
                                knownSones.add(friend);
                        }
                }
-               synchronized (knownPosts) {
-                       for (Post post : posts) {
-                               knownPosts.add(post.getId());
-                       }
+               for (Post post : posts) {
+                       post.setKnown(true);
                }
-               synchronized (knownReplies) {
-                       for (PostReply reply : replies) {
-                               knownReplies.add(reply.getId());
-                       }
+               for (PostReply reply : replies) {
+                       reply.setKnown(true);
                }
-       }
 
-       /**
-        * Creates a new post.
-        *
-        * @param sone
-        *            The Sone that creates the post
-        * @param text
-        *            The text of the post
-        * @return The created post
-        */
-       public Post createPost(Sone sone, String text) {
-               return createPost(sone, System.currentTimeMillis(), text);
-       }
-
-       /**
-        * Creates a new post.
-        *
-        * @param sone
-        *            The Sone that creates the post
-        * @param time
-        *            The time of the post
-        * @param text
-        *            The text of the post
-        * @return The created post
-        */
-       public Post createPost(Sone sone, long time, String text) {
-               return createPost(sone, null, time, text);
+               logger.info(String.format("Sone loaded successfully: %s", sone));
        }
 
        /**
@@ -1561,50 +1104,24 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
         *            The text of the post
         * @return The created post
         */
-       public Post createPost(Sone sone, Sone recipient, String text) {
-               return createPost(sone, recipient, System.currentTimeMillis(), text);
-       }
-
-       /**
-        * Creates a new post.
-        *
-        * @param sone
-        *            The Sone that creates the post
-        * @param recipient
-        *            The recipient Sone, or {@code null} if this post does not have
-        *            a recipient
-        * @param time
-        *            The time of the post
-        * @param text
-        *            The text of the post
-        * @return The created post
-        */
-       public Post createPost(Sone sone, Sone recipient, long time, String text) {
-               Validation.begin().isNotNull("Text", text).check().isGreater("Text Length", text.length(), 0).check();
-               if (!isLocalSone(sone)) {
+       public Post createPost(Sone sone, Optional<Sone> recipient, String text) {
+               checkNotNull(text, "text must not be null");
+               checkArgument(text.trim().length() > 0, "text must not be empty");
+               if (!sone.isLocal()) {
                        logger.log(Level.FINE, String.format("Tried to create post for non-local Sone: %s", sone));
                        return null;
                }
-               final Post post = new PostImpl(sone, time, text);
-               if (recipient != null) {
-                       post.setRecipient(recipient);
-               }
-               synchronized (posts) {
-                       posts.put(post.getId(), post);
+               PostBuilder postBuilder = database.newPostBuilder();
+               postBuilder.from(sone.getId()).randomId().currentTime().withText(text.trim());
+               if (recipient.isPresent()) {
+                       postBuilder.to(recipient.get().getId());
                }
-               coreListenerManager.fireNewPostFound(post);
+               final Post post = postBuilder.build();
+               database.storePost(post);
+               eventBus.post(new NewPostFoundEvent(post));
                sone.addPost(post);
                touchConfiguration();
-               localElementTicker.registerEvent(System.currentTimeMillis() + 10 * 1000, new Runnable() {
-
-                       /**
-                        * {@inheritDoc}
-                        */
-                       @Override
-                       public void run() {
-                               markPostKnown(post);
-                       }
-               }, "Mark " + post + " read.");
+               localElementTicker.schedule(new MarkPostKnown(post), 10, TimeUnit.SECONDS);
                return post;
        }
 
@@ -1615,15 +1132,12 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
         *            The post to delete
         */
        public void deletePost(Post post) {
-               if (!isLocalSone(post.getSone())) {
+               if (!post.getSone().isLocal()) {
                        logger.log(Level.WARNING, String.format("Tried to delete post of non-local Sone: %s", post.getSone()));
                        return;
                }
-               post.getSone().removePost(post);
-               synchronized (posts) {
-                       posts.remove(post.getId());
-               }
-               coreListenerManager.firePostRemoved(post);
+               database.removePost(post);
+               eventBus.post(new PostRemovedEvent(post));
                markPostKnown(post);
                touchConfiguration();
        }
@@ -1637,37 +1151,15 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
         */
        public void markPostKnown(Post post) {
                post.setKnown(true);
-               synchronized (knownPosts) {
-                       coreListenerManager.fireMarkPostKnown(post);
-                       if (knownPosts.add(post.getId())) {
-                               touchConfiguration();
-                       }
-               }
-               for (PostReply reply : getReplies(post)) {
+               eventBus.post(new MarkPostKnownEvent(post));
+               touchConfiguration();
+               for (PostReply reply : getReplies(post.getId())) {
                        markReplyKnown(reply);
                }
        }
 
-       /**
-        * Bookmarks the given post.
-        *
-        * @param post
-        *            The post to bookmark
-        */
-       public void bookmark(Post post) {
-               bookmarkPost(post.getId());
-       }
-
-       /**
-        * Bookmarks the post with the given ID.
-        *
-        * @param id
-        *            The ID of the post to bookmark
-        */
-       public void bookmarkPost(String id) {
-               synchronized (bookmarkedPosts) {
-                       bookmarkedPosts.add(id);
-               }
+       public void bookmarkPost(Post post) {
+               database.bookmarkPost(post);
        }
 
        /**
@@ -1676,20 +1168,8 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
         * @param post
         *            The post to unbookmark
         */
-       public void unbookmark(Post post) {
-               unbookmarkPost(post.getId());
-       }
-
-       /**
-        * Removes the post with the given ID from the bookmarks.
-        *
-        * @param id
-        *            The ID of the post to unbookmark
-        */
-       public void unbookmarkPost(String id) {
-               synchronized (bookmarkedPosts) {
-                       bookmarkedPosts.remove(id);
-               }
+       public void unbookmarkPost(Post post) {
+               database.unbookmarkPost(post);
        }
 
        /**
@@ -1704,47 +1184,20 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
         * @return The created reply
         */
        public PostReply createReply(Sone sone, Post post, String text) {
-               return createReply(sone, post, System.currentTimeMillis(), text);
-       }
-
-       /**
-        * Creates a new reply.
-        *
-        * @param sone
-        *            The Sone that creates the reply
-        * @param post
-        *            The post that this reply refers to
-        * @param time
-        *            The time of the reply
-        * @param text
-        *            The text of the reply
-        * @return The created reply
-        */
-       public PostReply createReply(Sone sone, Post post, long time, String text) {
-               Validation.begin().isNotNull("Text", text).check().isGreater("Text Length", text.trim().length(), 0).check();
-               if (!isLocalSone(sone)) {
+               checkNotNull(text, "text must not be null");
+               checkArgument(text.trim().length() > 0, "text must not be empty");
+               if (!sone.isLocal()) {
                        logger.log(Level.FINE, String.format("Tried to create reply for non-local Sone: %s", sone));
                        return null;
                }
-               final PostReply reply = new PostReply(sone, post, System.currentTimeMillis(), text);
-               synchronized (replies) {
-                       replies.put(reply.getId(), reply);
-               }
-               synchronized (knownReplies) {
-                       coreListenerManager.fireNewReplyFound(reply);
-               }
+               PostReplyBuilder postReplyBuilder = postReplyBuilder();
+               postReplyBuilder.randomId().from(sone.getId()).to(post.getId()).currentTime().withText(text.trim());
+               final PostReply reply = postReplyBuilder.build();
+               database.storePostReply(reply);
+               eventBus.post(new NewPostReplyFoundEvent(reply));
                sone.addReply(reply);
                touchConfiguration();
-               localElementTicker.registerEvent(System.currentTimeMillis() + 10 * 1000, new Runnable() {
-
-                       /**
-                        * {@inheritDoc}
-                        */
-                       @Override
-                       public void run() {
-                               markReplyKnown(reply);
-                       }
-               }, "Mark " + reply + " read.");
+               localElementTicker.schedule(new MarkReplyKnown(reply), 10, TimeUnit.SECONDS);
                return reply;
        }
 
@@ -1756,17 +1209,12 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
         */
        public void deleteReply(PostReply reply) {
                Sone sone = reply.getSone();
-               if (!isLocalSone(sone)) {
+               if (!sone.isLocal()) {
                        logger.log(Level.FINE, String.format("Tried to delete non-local reply: %s", reply));
                        return;
                }
-               synchronized (replies) {
-                       replies.remove(reply.getId());
-               }
-               synchronized (knownReplies) {
-                       markReplyKnown(reply);
-                       knownReplies.remove(reply.getId());
-               }
+               database.removePostReply(reply);
+               markReplyKnown(reply);
                sone.removeReply(reply);
                touchConfiguration();
        }
@@ -1779,27 +1227,15 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
         *            The reply to mark as known
         */
        public void markReplyKnown(PostReply reply) {
+               boolean previouslyKnown = reply.isKnown();
                reply.setKnown(true);
-               synchronized (knownReplies) {
-                       coreListenerManager.fireMarkReplyKnown(reply);
-                       if (knownReplies.add(reply.getId())) {
-                               touchConfiguration();
-                       }
+               eventBus.post(new MarkPostReplyKnownEvent(reply));
+               if (!previouslyKnown) {
+                       touchConfiguration();
                }
        }
 
        /**
-        * Creates a new top-level album for the given Sone.
-        *
-        * @param sone
-        *            The Sone to create the album for
-        * @return The new album
-        */
-       public Album createAlbum(Sone sone) {
-               return createAlbum(sone, null);
-       }
-
-       /**
         * Creates a new album for the given Sone.
         *
         * @param sone
@@ -1810,16 +1246,9 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
         * @return The new album
         */
        public Album createAlbum(Sone sone, Album parent) {
-               Album album = new Album();
-               synchronized (albums) {
-                       albums.put(album.getId(), album);
-               }
-               album.setSone(sone);
-               if (parent != null) {
-                       parent.addAlbum(album);
-               } else {
-                       sone.addAlbum(album);
-               }
+               Album album = database.newAlbumBuilder().randomId().by(sone).build();
+               database.storeAlbum(album);
+               parent.addAlbum(album);
                return album;
        }
 
@@ -1831,18 +1260,13 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
         *            The album to remove
         */
        public void deleteAlbum(Album album) {
-               Validation.begin().isNotNull("Album", album).check().is("Local Sone", isLocalSone(album.getSone())).check();
+               checkNotNull(album, "album must not be null");
+               checkArgument(album.getSone().isLocal(), "album’s Sone must be a local Sone");
                if (!album.isEmpty()) {
                        return;
                }
-               if (album.getParent() == null) {
-                       album.getSone().removeAlbum(album);
-               } else {
-                       album.getParent().removeAlbum(album);
-               }
-               synchronized (albums) {
-                       albums.remove(album.getId());
-               }
+               album.getParent().removeAlbum(album);
+               database.removeAlbum(album);
                touchConfiguration();
        }
 
@@ -1858,12 +1282,14 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
         * @return The newly created image
         */
        public Image createImage(Sone sone, Album album, TemporaryImage temporaryImage) {
-               Validation.begin().isNotNull("Sone", sone).isNotNull("Album", album).isNotNull("Temporary Image", temporaryImage).check().is("Local Sone", isLocalSone(sone)).check().isEqual("Owner and Album Owner", sone, album.getSone()).check();
-               Image image = new Image(temporaryImage.getId()).setSone(sone).setCreationTime(System.currentTimeMillis());
+               checkNotNull(sone, "sone must not be null");
+               checkNotNull(album, "album must not be null");
+               checkNotNull(temporaryImage, "temporaryImage must not be null");
+               checkArgument(sone.isLocal(), "sone must be a local Sone");
+               checkArgument(sone.equals(album.getSone()), "album must belong to the given Sone");
+               Image image = database.newImageBuilder().withId(temporaryImage.getId()).build().modify().setSone(sone).setCreationTime(System.currentTimeMillis()).update();
                album.addImage(image);
-               synchronized (images) {
-                       images.put(image.getId(), image);
-               }
+               database.storeImage(image);
                imageInserter.insertImage(temporaryImage, image);
                return image;
        }
@@ -1872,17 +1298,16 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
         * Deletes the given image. This method will also delete a matching
         * temporary image.
         *
-        * @see #deleteTemporaryImage(TemporaryImage)
+        * @see #deleteTemporaryImage(String)
         * @param image
         *            The image to delete
         */
        public void deleteImage(Image image) {
-               Validation.begin().isNotNull("Image", image).check().is("Local Sone", isLocalSone(image.getSone())).check();
+               checkNotNull(image, "image must not be null");
+               checkArgument(image.getSone().isLocal(), "image must belong to a local Sone");
                deleteTemporaryImage(image.getId());
                image.getAlbum().removeImage(image);
-               synchronized (images) {
-                       images.remove(image.getId());
-               }
+               database.removeImage(image);
                touchConfiguration();
        }
 
@@ -1905,24 +1330,13 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
        }
 
        /**
-        * Deletes the given temporary image.
-        *
-        * @param temporaryImage
-        *            The temporary image to delete
-        */
-       public void deleteTemporaryImage(TemporaryImage temporaryImage) {
-               Validation.begin().isNotNull("Temporary Image", temporaryImage).check();
-               deleteTemporaryImage(temporaryImage.getId());
-       }
-
-       /**
         * Deletes the temporary image with the given ID.
         *
         * @param imageId
         *            The ID of the temporary image to delete
         */
        public void deleteTemporaryImage(String imageId) {
-               Validation.begin().isNotNull("Temporary Image ID", imageId).check();
+               checkNotNull(imageId, "imageId must not be null");
                synchronized (temporaryImages) {
                        temporaryImages.remove(imageId);
                }
@@ -1951,9 +1365,11 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
        @Override
        public void serviceStart() {
                loadConfiguration();
-               updateChecker.addUpdateListener(this);
                updateChecker.start();
+               identityManager.start();
+               webOfTrustUpdater.init();
                webOfTrustUpdater.start();
+               database.start();
        }
 
        /**
@@ -1980,18 +1396,20 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
         */
        @Override
        public void serviceStop() {
-               synchronized (localSones) {
+               localElementTicker.shutdownNow();
+               synchronized (soneInserters) {
                        for (Entry<Sone, SoneInserter> soneInserter : soneInserters.entrySet()) {
-                               soneInserter.getValue().removeSoneInsertListener(this);
                                soneInserter.getValue().stop();
                                saveSone(soneInserter.getKey());
                        }
                }
                saveConfiguration();
+               database.stop();
                webOfTrustUpdater.stop();
                updateChecker.stop();
-               updateChecker.removeUpdateListener(this);
                soneDownloader.stop();
+               soneDownloaders.shutdown();
+               identityManager.stop();
        }
 
        //
@@ -2006,7 +1424,7 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
         *            The Sone to save
         */
        private synchronized void saveSone(Sone sone) {
-               if (!isLocalSone(sone)) {
+               if (!sone.isLocal()) {
                        logger.log(Level.FINE, String.format("Tried to save non-local Sone: %s", sone));
                        return;
                }
@@ -2046,7 +1464,7 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
                        for (Post post : sone.getPosts()) {
                                String postPrefix = sonePrefix + "/Posts/" + postCounter++;
                                configuration.getStringValue(postPrefix + "/ID").setValue(post.getId());
-                               configuration.getStringValue(postPrefix + "/Recipient").setValue((post.getRecipient() != null) ? post.getRecipient().getId() : null);
+                               configuration.getStringValue(postPrefix + "/Recipient").setValue(post.getRecipientId().orNull());
                                configuration.getLongValue(postPrefix + "/Time").setValue(post.getTime());
                                configuration.getStringValue(postPrefix + "/Text").setValue(post.getText());
                        }
@@ -2057,7 +1475,7 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
                        for (PostReply reply : sone.getReplies()) {
                                String replyPrefix = sonePrefix + "/Replies/" + replyCounter++;
                                configuration.getStringValue(replyPrefix + "/ID").setValue(reply.getId());
-                               configuration.getStringValue(replyPrefix + "/Post/ID").setValue(reply.getPost().getId());
+                               configuration.getStringValue(replyPrefix + "/Post/ID").setValue(reply.getPostId());
                                configuration.getLongValue(replyPrefix + "/Time").setValue(reply.getTime());
                                configuration.getStringValue(replyPrefix + "/Text").setValue(reply.getText());
                        }
@@ -2085,7 +1503,7 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
                        configuration.getStringValue(sonePrefix + "/Friends/" + friendCounter + "/ID").setValue(null);
 
                        /* save albums. first, collect in a flat structure, top-level first. */
-                       List<Album> albums = sone.getAllAlbums();
+                       List<Album> albums = FluentIterable.from(sone.getRootAlbum().getAlbums()).transformAndConcat(Album.FLATTENER).toList();
 
                        int albumCounter = 0;
                        for (Album album : albums) {
@@ -2093,7 +1511,7 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
                                configuration.getStringValue(albumPrefix + "/ID").setValue(album.getId());
                                configuration.getStringValue(albumPrefix + "/Title").setValue(album.getTitle());
                                configuration.getStringValue(albumPrefix + "/Description").setValue(album.getDescription());
-                               configuration.getStringValue(albumPrefix + "/Parent").setValue(album.getParent() == null ? null : album.getParent().getId());
+                               configuration.getStringValue(albumPrefix + "/Parent").setValue(album.getParent().equals(sone.getRootAlbum()) ? null : album.getParent().getId());
                                configuration.getStringValue(albumPrefix + "/AlbumImage").setValue(album.getAlbumImage() == null ? null : album.getAlbumImage().getId());
                        }
                        configuration.getStringValue(sonePrefix + "/Albums/" + albumCounter + "/ID").setValue(null);
@@ -2119,12 +1537,12 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
                        configuration.getStringValue(sonePrefix + "/Images/" + imageCounter + "/ID").setValue(null);
 
                        /* save options. */
-                       configuration.getBooleanValue(sonePrefix + "/Options/AutoFollow").setValue(sone.getOptions().getBooleanOption("AutoFollow").getReal());
-                       configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewSones").setValue(sone.getOptions().getBooleanOption("ShowNotification/NewSones").getReal());
-                       configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewPosts").setValue(sone.getOptions().getBooleanOption("ShowNotification/NewPosts").getReal());
-                       configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewReplies").setValue(sone.getOptions().getBooleanOption("ShowNotification/NewReplies").getReal());
-                       configuration.getBooleanValue(sonePrefix + "/Options/EnableSoneInsertNotifications").setValue(sone.getOptions().getBooleanOption("EnableSoneInsertNotifications").getReal());
-                       configuration.getStringValue(sonePrefix + "/Options/ShowCustomAvatars").setValue(sone.getOptions().<ShowCustomAvatars> getEnumOption("ShowCustomAvatars").get().name());
+                       configuration.getBooleanValue(sonePrefix + "/Options/AutoFollow").setValue(sone.getOptions().isAutoFollow());
+                       configuration.getBooleanValue(sonePrefix + "/Options/EnableSoneInsertNotifications").setValue(sone.getOptions().isSoneInsertNotificationEnabled());
+                       configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewSones").setValue(sone.getOptions().isShowNewSoneNotifications());
+                       configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewPosts").setValue(sone.getOptions().isShowNewPostNotifications());
+                       configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewReplies").setValue(sone.getOptions().isShowNewReplyNotifications());
+                       configuration.getStringValue(sonePrefix + "/Options/ShowCustomAvatars").setValue(sone.getOptions().getShowCustomAvatars().name());
 
                        configuration.save();
 
@@ -2150,18 +1568,7 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
 
                /* store the options first. */
                try {
-                       configuration.getIntValue("Option/ConfigurationVersion").setValue(0);
-                       configuration.getIntValue("Option/InsertionDelay").setValue(options.getIntegerOption("InsertionDelay").getReal());
-                       configuration.getIntValue("Option/PostsPerPage").setValue(options.getIntegerOption("PostsPerPage").getReal());
-                       configuration.getIntValue("Option/ImagesPerPage").setValue(options.getIntegerOption("ImagesPerPage").getReal());
-                       configuration.getIntValue("Option/CharactersPerPost").setValue(options.getIntegerOption("CharactersPerPost").getReal());
-                       configuration.getIntValue("Option/PostCutOffLength").setValue(options.getIntegerOption("PostCutOffLength").getReal());
-                       configuration.getBooleanValue("Option/RequireFullAccess").setValue(options.getBooleanOption("RequireFullAccess").getReal());
-                       configuration.getIntValue("Option/PositiveTrust").setValue(options.getIntegerOption("PositiveTrust").getReal());
-                       configuration.getIntValue("Option/NegativeTrust").setValue(options.getIntegerOption("NegativeTrust").getReal());
-                       configuration.getStringValue("Option/TrustComment").setValue(options.getStringOption("TrustComment").getReal());
-                       configuration.getBooleanValue("Option/ActivateFcpInterface").setValue(options.getBooleanOption("ActivateFcpInterface").getReal());
-                       configuration.getIntValue("Option/FcpFullAccessRequired").setValue(options.getIntegerOption("FcpFullAccessRequired").getReal());
+                       preferences.saveTo(configuration);
 
                        /* save known Sones. */
                        int soneCounter = 0;
@@ -2175,8 +1582,8 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
                        /* save Sone following times. */
                        soneCounter = 0;
                        synchronized (soneFollowingTimes) {
-                               for (Entry<Sone, Long> soneFollowingTime : soneFollowingTimes.entrySet()) {
-                                       configuration.getStringValue("SoneFollowingTimes/" + soneCounter + "/Sone").setValue(soneFollowingTime.getKey().getId());
+                               for (Entry<String, Long> soneFollowingTime : soneFollowingTimes.entrySet()) {
+                                       configuration.getStringValue("SoneFollowingTimes/" + soneCounter + "/Sone").setValue(soneFollowingTime.getKey());
                                        configuration.getLongValue("SoneFollowingTimes/" + soneCounter + "/Time").setValue(soneFollowingTime.getValue());
                                        ++soneCounter;
                                }
@@ -2184,37 +1591,15 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
                        }
 
                        /* save known posts. */
-                       int postCounter = 0;
-                       synchronized (knownPosts) {
-                               for (String knownPostId : knownPosts) {
-                                       configuration.getStringValue("KnownPosts/" + postCounter++ + "/ID").setValue(knownPostId);
-                               }
-                               configuration.getStringValue("KnownPosts/" + postCounter + "/ID").setValue(null);
-                       }
-
-                       /* save known replies. */
-                       int replyCounter = 0;
-                       synchronized (knownReplies) {
-                               for (String knownReplyId : knownReplies) {
-                                       configuration.getStringValue("KnownReplies/" + replyCounter++ + "/ID").setValue(knownReplyId);
-                               }
-                               configuration.getStringValue("KnownReplies/" + replyCounter + "/ID").setValue(null);
-                       }
-
-                       /* save bookmarked posts. */
-                       int bookmarkedPostCounter = 0;
-                       synchronized (bookmarkedPosts) {
-                               for (String bookmarkedPostId : bookmarkedPosts) {
-                                       configuration.getStringValue("Bookmarks/Post/" + bookmarkedPostCounter++ + "/ID").setValue(bookmarkedPostId);
-                               }
-                       }
-                       configuration.getStringValue("Bookmarks/Post/" + bookmarkedPostCounter++ + "/ID").setValue(null);
+                       database.save();
 
                        /* now save it. */
                        configuration.save();
 
                } catch (ConfigurationException ce1) {
                        logger.log(Level.SEVERE, "Could not store configuration!", ce1);
+               } catch (DatabaseException de1) {
+                       logger.log(Level.SEVERE, "Could not save database!", de1);
                } finally {
                        synchronized (configuration) {
                                storingConfiguration = false;
@@ -2225,54 +1610,8 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
        /**
         * Loads the configuration.
         */
-       @SuppressWarnings("unchecked")
        private void loadConfiguration() {
-               /* create options. */
-               options.addIntegerOption("InsertionDelay", new DefaultOption<Integer>(60, new IntegerRangeValidator(0, Integer.MAX_VALUE), new OptionWatcher<Integer>() {
-
-                       @Override
-                       public void optionChanged(Option<Integer> option, Integer oldValue, Integer newValue) {
-                               SoneInserter.setInsertionDelay(newValue);
-                       }
-
-               }));
-               options.addIntegerOption("PostsPerPage", new DefaultOption<Integer>(10, new IntegerRangeValidator(1, Integer.MAX_VALUE)));
-               options.addIntegerOption("ImagesPerPage", new DefaultOption<Integer>(9, new IntegerRangeValidator(1, Integer.MAX_VALUE)));
-               options.addIntegerOption("CharactersPerPost", new DefaultOption<Integer>(400, new OrValidator<Integer>(new IntegerRangeValidator(50, Integer.MAX_VALUE), new EqualityValidator<Integer>(-1))));
-               options.addIntegerOption("PostCutOffLength", new DefaultOption<Integer>(200, new OrValidator<Integer>(new IntegerRangeValidator(50, Integer.MAX_VALUE), new EqualityValidator<Integer>(-1))));
-               options.addBooleanOption("RequireFullAccess", new DefaultOption<Boolean>(false));
-               options.addIntegerOption("PositiveTrust", new DefaultOption<Integer>(75, new IntegerRangeValidator(0, 100)));
-               options.addIntegerOption("NegativeTrust", new DefaultOption<Integer>(-25, new IntegerRangeValidator(-100, 100)));
-               options.addStringOption("TrustComment", new DefaultOption<String>("Set from Sone Web Interface"));
-               options.addBooleanOption("ActivateFcpInterface", new DefaultOption<Boolean>(false, new OptionWatcher<Boolean>() {
-
-                       @Override
-                       @SuppressWarnings("synthetic-access")
-                       public void optionChanged(Option<Boolean> option, Boolean oldValue, Boolean newValue) {
-                               fcpInterface.setActive(newValue);
-                       }
-               }));
-               options.addIntegerOption("FcpFullAccessRequired", new DefaultOption<Integer>(2, new OptionWatcher<Integer>() {
-
-                       @Override
-                       @SuppressWarnings("synthetic-access")
-                       public void optionChanged(Option<Integer> option, Integer oldValue, Integer newValue) {
-                               fcpInterface.setFullAccessRequired(FullAccessRequired.values()[newValue]);
-                       }
-
-               }));
-
-               loadConfigurationValue("InsertionDelay");
-               loadConfigurationValue("PostsPerPage");
-               loadConfigurationValue("ImagesPerPage");
-               loadConfigurationValue("CharactersPerPost");
-               loadConfigurationValue("PostCutOffLength");
-               options.getBooleanOption("RequireFullAccess").set(configuration.getBooleanValue("Option/RequireFullAccess").getValue(null));
-               loadConfigurationValue("PositiveTrust");
-               loadConfigurationValue("NegativeTrust");
-               options.getStringOption("TrustComment").set(configuration.getStringValue("Option/TrustComment").getValue(null));
-               options.getBooleanOption("ActivateFcpInterface").set(configuration.getBooleanValue("Option/ActivateFcpInterface").getValue(null));
-               options.getIntegerOption("FcpFullAccessRequired").set(configuration.getIntValue("Option/FcpFullAccessRequired").getValue(null));
+               new PreferencesLoader(preferences).loadFrom(configuration);
 
                /* load known Sones. */
                int soneCounter = 0;
@@ -2294,620 +1633,143 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
                                break;
                        }
                        long time = configuration.getLongValue("SoneFollowingTimes/" + soneCounter + "/Time").getValue(Long.MAX_VALUE);
-                       Sone followedSone = getSone(soneId);
-                       if (followedSone == null) {
-                               logger.log(Level.WARNING, String.format("Ignoring Sone with invalid ID: %s", soneId));
-                       } else {
-                               synchronized (soneFollowingTimes) {
-                                       soneFollowingTimes.put(getSone(soneId), time);
-                               }
+                       synchronized (soneFollowingTimes) {
+                               soneFollowingTimes.put(soneId, time);
                        }
                        ++soneCounter;
                }
-
-               /* load known posts. */
-               int postCounter = 0;
-               while (true) {
-                       String knownPostId = configuration.getStringValue("KnownPosts/" + postCounter++ + "/ID").getValue(null);
-                       if (knownPostId == null) {
-                               break;
-                       }
-                       synchronized (knownPosts) {
-                               knownPosts.add(knownPostId);
-                       }
-               }
-
-               /* load known replies. */
-               int replyCounter = 0;
-               while (true) {
-                       String knownReplyId = configuration.getStringValue("KnownReplies/" + replyCounter++ + "/ID").getValue(null);
-                       if (knownReplyId == null) {
-                               break;
-                       }
-                       synchronized (knownReplies) {
-                               knownReplies.add(knownReplyId);
-                       }
-               }
-
-               /* load bookmarked posts. */
-               int bookmarkedPostCounter = 0;
-               while (true) {
-                       String bookmarkedPostId = configuration.getStringValue("Bookmarks/Post/" + bookmarkedPostCounter++ + "/ID").getValue(null);
-                       if (bookmarkedPostId == null) {
-                               break;
-                       }
-                       synchronized (bookmarkedPosts) {
-                               bookmarkedPosts.add(bookmarkedPostId);
-                       }
-               }
-
        }
 
        /**
-        * Loads an {@link Integer} configuration value for the option with the
-        * given name, logging validation failures.
+        * Notifies the core that a new {@link OwnIdentity} was added.
         *
-        * @param optionName
-        *            The name of the option to load
-        */
-       private void loadConfigurationValue(String optionName) {
-               try {
-                       options.getIntegerOption(optionName).set(configuration.getIntValue("Option/" + optionName).getValue(null));
-               } catch (IllegalArgumentException iae1) {
-                       logger.log(Level.WARNING, String.format("Invalid value for %s in configuration, using default.", optionName));
-               }
-       }
-
-       /**
-        * Generate a Sone URI from the given URI and latest edition.
-        *
-        * @param uriString
-        *            The URI to derive the Sone URI from
-        * @return The derived URI
-        */
-       private static FreenetURI getSoneUri(String uriString) {
-               try {
-                       FreenetURI uri = new FreenetURI(uriString).setDocName("Sone").setMetaString(new String[0]);
-                       return uri;
-               } catch (MalformedURLException mue1) {
-                       logger.log(Level.WARNING, String.format("Could not create Sone URI from URI: %s", uriString), mue1);
-                       return null;
-               }
-       }
-
-       //
-       // INTERFACE IdentityListener
-       //
-
-       /**
-        * {@inheritDoc}
+        * @param ownIdentityAddedEvent
+        *            The event
         */
-       @Override
-       public void ownIdentityAdded(OwnIdentity ownIdentity) {
+       @Subscribe
+       public void ownIdentityAdded(OwnIdentityAddedEvent ownIdentityAddedEvent) {
+               OwnIdentity ownIdentity = ownIdentityAddedEvent.ownIdentity();
                logger.log(Level.FINEST, String.format("Adding OwnIdentity: %s", ownIdentity));
                if (ownIdentity.hasContext("Sone")) {
-                       trustedIdentities.put(ownIdentity, Collections.synchronizedSet(new HashSet<Identity>()));
                        addLocalSone(ownIdentity);
                }
        }
 
        /**
-        * {@inheritDoc}
+        * Notifies the core that an {@link OwnIdentity} was removed.
+        *
+        * @param ownIdentityRemovedEvent
+        *            The event
         */
-       @Override
-       public void ownIdentityRemoved(OwnIdentity ownIdentity) {
+       @Subscribe
+       public void ownIdentityRemoved(OwnIdentityRemovedEvent ownIdentityRemovedEvent) {
+               OwnIdentity ownIdentity = ownIdentityRemovedEvent.ownIdentity();
                logger.log(Level.FINEST, String.format("Removing OwnIdentity: %s", ownIdentity));
-               trustedIdentities.remove(ownIdentity);
+               trustedIdentities.removeAll(ownIdentity);
        }
 
        /**
-        * {@inheritDoc}
+        * Notifies the core that a new {@link Identity} was added.
+        *
+        * @param identityAddedEvent
+        *            The event
         */
-       @Override
-       public void identityAdded(OwnIdentity ownIdentity, Identity identity) {
+       @Subscribe
+       public void identityAdded(IdentityAddedEvent identityAddedEvent) {
+               Identity identity = identityAddedEvent.identity();
                logger.log(Level.FINEST, String.format("Adding Identity: %s", identity));
-               trustedIdentities.get(ownIdentity).add(identity);
+               trustedIdentities.put(identityAddedEvent.ownIdentity(), identity);
                addRemoteSone(identity);
        }
 
        /**
-        * {@inheritDoc}
+        * Notifies the core that an {@link Identity} was updated.
+        *
+        * @param identityUpdatedEvent
+        *            The event
         */
-       @Override
-       public void identityUpdated(OwnIdentity ownIdentity, final Identity identity) {
-               soneDownloaders.execute(new Runnable() {
-
-                       @Override
-                       @SuppressWarnings("synthetic-access")
-                       public void run() {
-                               Sone sone = getRemoteSone(identity.getId(), false);
-                               sone.setIdentity(identity);
-                               sone.setLatestEdition(Numbers.safeParseLong(identity.getProperty("Sone.LatestEdition"), sone.getLatestEdition()));
-                               soneDownloader.addSone(sone);
-                               soneDownloader.fetchSone(sone);
-                       }
-               });
+       @Subscribe
+       public void identityUpdated(IdentityUpdatedEvent identityUpdatedEvent) {
+               Identity identity = identityUpdatedEvent.identity();
+               final Sone sone = getRemoteSone(identity.getId());
+               if (sone.isLocal()) {
+                       return;
+               }
+               sone.setLatestEdition(fromNullable(tryParse(identity.getProperty("Sone.LatestEdition"))).or(sone.getLatestEdition()));
+               soneDownloader.addSone(sone);
+               soneDownloaders.execute(soneDownloader.fetchSoneAction(sone));
        }
 
        /**
-        * {@inheritDoc}
+        * Notifies the core that an {@link Identity} was removed.
+        *
+        * @param identityRemovedEvent
+        *            The event
         */
-       @Override
-       public void identityRemoved(OwnIdentity ownIdentity, Identity identity) {
-               trustedIdentities.get(ownIdentity).remove(identity);
-               boolean foundIdentity = false;
-               for (Entry<OwnIdentity, Set<Identity>> trustedIdentity : trustedIdentities.entrySet()) {
+       @Subscribe
+       public void identityRemoved(IdentityRemovedEvent identityRemovedEvent) {
+               OwnIdentity ownIdentity = identityRemovedEvent.ownIdentity();
+               Identity identity = identityRemovedEvent.identity();
+               trustedIdentities.remove(ownIdentity, identity);
+               for (Entry<OwnIdentity, Collection<Identity>> trustedIdentity : trustedIdentities.asMap().entrySet()) {
                        if (trustedIdentity.getKey().equals(ownIdentity)) {
                                continue;
                        }
                        if (trustedIdentity.getValue().contains(identity)) {
-                               foundIdentity = true;
+                               return;
                        }
                }
-               if (foundIdentity) {
-                       /* some local identity still trusts this identity, don’t remove. */
-                       return;
-               }
-               Sone sone = getSone(identity.getId(), false);
-               if (sone == null) {
+               Optional<Sone> sone = getSone(identity.getId());
+               if (!sone.isPresent()) {
                        /* TODO - we don’t have the Sone anymore. should this happen? */
                        return;
                }
-               synchronized (posts) {
-                       synchronized (knownPosts) {
-                               for (Post post : sone.getPosts()) {
-                                       posts.remove(post.getId());
-                                       coreListenerManager.firePostRemoved(post);
-                               }
-                       }
-               }
-               synchronized (replies) {
-                       synchronized (knownReplies) {
-                               for (PostReply reply : sone.getReplies()) {
-                                       replies.remove(reply.getId());
-                                       coreListenerManager.fireReplyRemoved(reply);
-                               }
-                       }
-               }
-               synchronized (remoteSones) {
-                       remoteSones.remove(identity.getId());
-               }
-               coreListenerManager.fireSoneRemoved(sone);
+               database.removeSone(sone.get());
+               eventBus.post(new SoneRemovedEvent(sone.get()));
        }
 
-       //
-       // INTERFACE UpdateListener
-       //
-
        /**
-        * {@inheritDoc}
+        * Deletes the temporary image.
+        *
+        * @param imageInsertFinishedEvent
+        *            The event
         */
-       @Override
-       public void updateFound(Version version, long releaseTime, long latestEdition) {
-               coreListenerManager.fireUpdateFound(version, releaseTime, latestEdition);
+       @Subscribe
+       public void imageInsertFinished(ImageInsertFinishedEvent imageInsertFinishedEvent) {
+               logger.log(Level.WARNING, String.format("Image insert finished for %s: %s", imageInsertFinishedEvent.image(), imageInsertFinishedEvent.resultingUri()));
+               imageInsertFinishedEvent.image().modify().setKey(imageInsertFinishedEvent.resultingUri().toString()).update();
+               deleteTemporaryImage(imageInsertFinishedEvent.image().getId());
+               touchConfiguration();
        }
 
-       //
-       // INTERFACE ImageInsertListener
-       //
+       @VisibleForTesting
+       class MarkPostKnown implements Runnable {
 
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public void insertStarted(Sone sone) {
-               coreListenerManager.fireSoneInserting(sone);
-       }
+               private final Post post;
 
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public void insertFinished(Sone sone, long insertDuration) {
-               coreListenerManager.fireSoneInserted(sone, insertDuration);
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public void insertAborted(Sone sone, Throwable cause) {
-               coreListenerManager.fireSoneInsertAborted(sone, cause);
-       }
+               public MarkPostKnown(Post post) {
+                       this.post = post;
+               }
 
-       //
-       // SONEINSERTLISTENER METHODS
-       //
+               @Override
+               public void run() {
+                       markPostKnown(post);
+               }
 
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public void imageInsertStarted(Image image) {
-               logger.log(Level.WARNING, String.format("Image insert started for %s...", image));
-               coreListenerManager.fireImageInsertStarted(image);
        }
 
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public void imageInsertAborted(Image image) {
-               logger.log(Level.WARNING, String.format("Image insert aborted for %s.", image));
-               coreListenerManager.fireImageInsertAborted(image);
-       }
+       @VisibleForTesting
+       class MarkReplyKnown implements Runnable {
 
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public void imageInsertFinished(Image image, FreenetURI key) {
-               logger.log(Level.WARNING, String.format("Image insert finished for %s: %s", image, key));
-               image.setKey(key.toString());
-               deleteTemporaryImage(image.getId());
-               touchConfiguration();
-               coreListenerManager.fireImageInsertFinished(image);
-       }
+               private final PostReply postReply;
 
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public void imageInsertFailed(Image image, Throwable cause) {
-               logger.log(Level.WARNING, String.format("Image insert failed for %s." + image), cause);
-               coreListenerManager.fireImageInsertFailed(image, cause);
-       }
+               public MarkReplyKnown(PostReply postReply) {
+                       this.postReply = postReply;
+               }
 
-       /**
-        * Convenience interface for external classes that want to access the core’s
-        * configuration.
-        *
-        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
-        */
-       public static class Preferences {
-
-               /** The wrapped options. */
-               private final Options options;
-
-               /**
-                * Creates a new preferences object wrapped around the given options.
-                *
-                * @param options
-                *            The options to wrap
-                */
-               public Preferences(Options options) {
-                       this.options = options;
-               }
-
-               /**
-                * Returns the insertion delay.
-                *
-                * @return The insertion delay
-                */
-               public int getInsertionDelay() {
-                       return options.getIntegerOption("InsertionDelay").get();
-               }
-
-               /**
-                * Validates the given insertion delay.
-                *
-                * @param insertionDelay
-                *            The insertion delay to validate
-                * @return {@code true} if the given insertion delay was valid,
-                *         {@code false} otherwise
-                */
-               public boolean validateInsertionDelay(Integer insertionDelay) {
-                       return options.getIntegerOption("InsertionDelay").validate(insertionDelay);
-               }
-
-               /**
-                * Sets the insertion delay
-                *
-                * @param insertionDelay
-                *            The new insertion delay, or {@code null} to restore it to
-                *            the default value
-                * @return This preferences
-                */
-               public Preferences setInsertionDelay(Integer insertionDelay) {
-                       options.getIntegerOption("InsertionDelay").set(insertionDelay);
-                       return this;
-               }
-
-               /**
-                * Returns the number of posts to show per page.
-                *
-                * @return The number of posts to show per page
-                */
-               public int getPostsPerPage() {
-                       return options.getIntegerOption("PostsPerPage").get();
-               }
-
-               /**
-                * Validates the number of posts per page.
-                *
-                * @param postsPerPage
-                *            The number of posts per page
-                * @return {@code true} if the number of posts per page was valid,
-                *         {@code false} otherwise
-                */
-               public boolean validatePostsPerPage(Integer postsPerPage) {
-                       return options.getIntegerOption("PostsPerPage").validate(postsPerPage);
-               }
-
-               /**
-                * Sets the number of posts to show per page.
-                *
-                * @param postsPerPage
-                *            The number of posts to show per page
-                * @return This preferences object
-                */
-               public Preferences setPostsPerPage(Integer postsPerPage) {
-                       options.getIntegerOption("PostsPerPage").set(postsPerPage);
-                       return this;
-               }
-
-               /**
-                * Returns the number of images to show per page.
-                *
-                * @return The number of images to show per page
-                */
-               public int getImagesPerPage() {
-                       return options.getIntegerOption("ImagesPerPage").get();
-               }
-
-               /**
-                * Validates the number of images per page.
-                *
-                * @param imagesPerPage
-                *            The number of images per page
-                * @return {@code true} if the number of images per page was valid,
-                *         {@code false} otherwise
-                */
-               public boolean validateImagesPerPage(Integer imagesPerPage) {
-                       return options.getIntegerOption("ImagesPerPage").validate(imagesPerPage);
-               }
-
-               /**
-                * Sets the number of images per page.
-                *
-                * @param imagesPerPage
-                *            The number of images per page
-                * @return This preferences object
-                */
-               public Preferences setImagesPerPage(Integer imagesPerPage) {
-                       options.getIntegerOption("ImagesPerPage").set(imagesPerPage);
-                       return this;
-               }
-
-               /**
-                * Returns the number of characters per post, or <code>-1</code> if the
-                * posts should not be cut off.
-                *
-                * @return The numbers of characters per post
-                */
-               public int getCharactersPerPost() {
-                       return options.getIntegerOption("CharactersPerPost").get();
-               }
-
-               /**
-                * Validates the number of characters per post.
-                *
-                * @param charactersPerPost
-                *            The number of characters per post
-                * @return {@code true} if the number of characters per post was valid,
-                *         {@code false} otherwise
-                */
-               public boolean validateCharactersPerPost(Integer charactersPerPost) {
-                       return options.getIntegerOption("CharactersPerPost").validate(charactersPerPost);
-               }
-
-               /**
-                * Sets the number of characters per post.
-                *
-                * @param charactersPerPost
-                *            The number of characters per post, or <code>-1</code> to
-                *            not cut off the posts
-                * @return This preferences objects
-                */
-               public Preferences setCharactersPerPost(Integer charactersPerPost) {
-                       options.getIntegerOption("CharactersPerPost").set(charactersPerPost);
-                       return this;
-               }
-
-               /**
-                * Returns the number of characters the shortened post should have.
-                *
-                * @return The number of characters of the snippet
-                */
-               public int getPostCutOffLength() {
-                       return options.getIntegerOption("PostCutOffLength").get();
-               }
-
-               /**
-                * Validates the number of characters after which to cut off the post.
-                *
-                * @param postCutOffLength
-                *            The number of characters of the snippet
-                * @return {@code true} if the number of characters of the snippet is
-                *         valid, {@code false} otherwise
-                */
-               public boolean validatePostCutOffLength(Integer postCutOffLength) {
-                       return options.getIntegerOption("PostCutOffLength").validate(postCutOffLength);
-               }
-
-               /**
-                * Sets the number of characters the shortened post should have.
-                *
-                * @param postCutOffLength
-                *            The number of characters of the snippet
-                * @return This preferences
-                */
-               public Preferences setPostCutOffLength(Integer postCutOffLength) {
-                       options.getIntegerOption("PostCutOffLength").set(postCutOffLength);
-                       return this;
-               }
-
-               /**
-                * Returns whether Sone requires full access to be even visible.
-                *
-                * @return {@code true} if Sone requires full access, {@code false}
-                *         otherwise
-                */
-               public boolean isRequireFullAccess() {
-                       return options.getBooleanOption("RequireFullAccess").get();
-               }
-
-               /**
-                * Sets whether Sone requires full access to be even visible.
-                *
-                * @param requireFullAccess
-                *            {@code true} if Sone requires full access, {@code false}
-                *            otherwise
-                */
-               public void setRequireFullAccess(Boolean requireFullAccess) {
-                       options.getBooleanOption("RequireFullAccess").set(requireFullAccess);
-               }
-
-               /**
-                * Returns the positive trust.
-                *
-                * @return The positive trust
-                */
-               public int getPositiveTrust() {
-                       return options.getIntegerOption("PositiveTrust").get();
-               }
-
-               /**
-                * Validates the positive trust.
-                *
-                * @param positiveTrust
-                *            The positive trust to validate
-                * @return {@code true} if the positive trust was valid, {@code false}
-                *         otherwise
-                */
-               public boolean validatePositiveTrust(Integer positiveTrust) {
-                       return options.getIntegerOption("PositiveTrust").validate(positiveTrust);
-               }
-
-               /**
-                * Sets the positive trust.
-                *
-                * @param positiveTrust
-                *            The new positive trust, or {@code null} to restore it to
-                *            the default vlaue
-                * @return This preferences
-                */
-               public Preferences setPositiveTrust(Integer positiveTrust) {
-                       options.getIntegerOption("PositiveTrust").set(positiveTrust);
-                       return this;
-               }
-
-               /**
-                * Returns the negative trust.
-                *
-                * @return The negative trust
-                */
-               public int getNegativeTrust() {
-                       return options.getIntegerOption("NegativeTrust").get();
-               }
-
-               /**
-                * Validates the negative trust.
-                *
-                * @param negativeTrust
-                *            The negative trust to validate
-                * @return {@code true} if the negative trust was valid, {@code false}
-                *         otherwise
-                */
-               public boolean validateNegativeTrust(Integer negativeTrust) {
-                       return options.getIntegerOption("NegativeTrust").validate(negativeTrust);
-               }
-
-               /**
-                * Sets the negative trust.
-                *
-                * @param negativeTrust
-                *            The negative trust, or {@code null} to restore it to the
-                *            default value
-                * @return The preferences
-                */
-               public Preferences setNegativeTrust(Integer negativeTrust) {
-                       options.getIntegerOption("NegativeTrust").set(negativeTrust);
-                       return this;
-               }
-
-               /**
-                * Returns the trust comment. This is the comment that is set in the web
-                * of trust when a trust value is assigned to an identity.
-                *
-                * @return The trust comment
-                */
-               public String getTrustComment() {
-                       return options.getStringOption("TrustComment").get();
-               }
-
-               /**
-                * Sets the trust comment.
-                *
-                * @param trustComment
-                *            The trust comment, or {@code null} to restore it to the
-                *            default value
-                * @return This preferences
-                */
-               public Preferences setTrustComment(String trustComment) {
-                       options.getStringOption("TrustComment").set(trustComment);
-                       return this;
-               }
-
-               /**
-                * Returns whether the {@link FcpInterface FCP interface} is currently
-                * active.
-                *
-                * @see FcpInterface#setActive(boolean)
-                * @return {@code true} if the FCP interface is currently active,
-                *         {@code false} otherwise
-                */
-               public boolean isFcpInterfaceActive() {
-                       return options.getBooleanOption("ActivateFcpInterface").get();
-               }
-
-               /**
-                * Sets whether the {@link FcpInterface FCP interface} is currently
-                * active.
-                *
-                * @see FcpInterface#setActive(boolean)
-                * @param fcpInterfaceActive
-                *            {@code true} to activate the FCP interface, {@code false}
-                *            to deactivate the FCP interface
-                * @return This preferences object
-                */
-               public Preferences setFcpInterfaceActive(boolean fcpInterfaceActive) {
-                       options.getBooleanOption("ActivateFcpInterface").set(fcpInterfaceActive);
-                       return this;
-               }
-
-               /**
-                * Returns the action level for which full access to the FCP interface
-                * is required.
-                *
-                * @return The action level for which full access to the FCP interface
-                *         is required
-                */
-               public FullAccessRequired getFcpFullAccessRequired() {
-                       return FullAccessRequired.values()[options.getIntegerOption("FcpFullAccessRequired").get()];
-               }
-
-               /**
-                * Sets the action level for which full access to the FCP interface is
-                * required
-                *
-                * @param fcpFullAccessRequired
-                *            The action level
-                * @return This preferences
-                */
-               public Preferences setFcpFullAccessRequired(FullAccessRequired fcpFullAccessRequired) {
-                       options.getIntegerOption("FcpFullAccessRequired").set((fcpFullAccessRequired != null) ? fcpFullAccessRequired.ordinal() : null);
-                       return this;
+               @Override
+               public void run() {
+                       markReplyKnown(postReply);
                }
 
        }