Store identities in database.
[Sone.git] / src / main / java / net / pterodactylus / sone / core / Core.java
index 03d331d..4bf8a8a 100644 (file)
 
 package net.pterodactylus.sone.core;
 
+import static com.google.common.base.Optional.of;
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Predicates.not;
+import static com.google.common.collect.FluentIterable.from;
+import static net.pterodactylus.sone.data.Identified.GET_ID;
+import static net.pterodactylus.sone.data.Sone.LOCAL_SONE_FILTER;
+import static net.pterodactylus.sone.data.Sone.TO_FREENET_URI;
 
-import java.net.MalformedURLException;
-import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -58,17 +62,19 @@ import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.PostReply;
 import net.pterodactylus.sone.data.Profile;
 import net.pterodactylus.sone.data.Profile.Field;
-import net.pterodactylus.sone.data.Reply;
+import net.pterodactylus.sone.data.Reply.Modifier.ReplyUpdated;
 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.database.Database;
 import net.pterodactylus.sone.database.DatabaseException;
+import net.pterodactylus.sone.database.ImageBuilder.ImageCreated;
 import net.pterodactylus.sone.database.PostBuilder;
-import net.pterodactylus.sone.database.PostProvider;
+import net.pterodactylus.sone.database.PostBuilder.PostCreated;
 import net.pterodactylus.sone.database.PostReplyBuilder;
-import net.pterodactylus.sone.database.PostReplyProvider;
+import net.pterodactylus.sone.database.PostReplyBuilder.PostReplyCreated;
+import net.pterodactylus.sone.database.SoneBuilder.SoneCreated;
 import net.pterodactylus.sone.database.SoneProvider;
 import net.pterodactylus.sone.fcp.FcpInterface;
 import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired;
@@ -89,34 +95,29 @@ import net.pterodactylus.util.number.Numbers;
 import net.pterodactylus.util.service.AbstractService;
 import net.pterodactylus.util.thread.NamedThreadFactory;
 
+import com.google.common.base.Function;
 import com.google.common.base.Optional;
 import com.google.common.base.Predicate;
 import com.google.common.base.Predicates;
-import com.google.common.collect.Collections2;
-import com.google.common.collect.FluentIterable;
 import com.google.common.collect.HashMultimap;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
 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 freenet.keys.FreenetURI;
-
 /**
  * The Sone core.
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
-public class Core extends AbstractService implements SoneProvider, PostProvider, PostReplyProvider {
+public class Core extends AbstractService implements SoneProvider {
 
        /** The logger. */
        private static final Logger logger = Logging.getLogger(Core.class);
 
-       /** The start time. */
-       private final long startupTime = System.currentTimeMillis();
-
        /** The options. */
        private final Options options = new Options();
 
@@ -188,12 +189,6 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
        /** Trusted identities, sorted by own identities. */
        private final Multimap<OwnIdentity, Identity> trustedIdentities = Multimaps.synchronizedSetMultimap(HashMultimap.<OwnIdentity, Identity>create());
 
-       /** 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>();
-
        /** All temporary images. */
        private final Map<String, TemporaryImage> temporaryImages = new HashMap<String, TemporaryImage>();
 
@@ -207,17 +202,17 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * Creates a new core.
         *
         * @param configuration
-        *            The configuration of the core
+        *              The configuration of the core
         * @param freenetInterface
-        *            The freenet interface
+        *              The freenet interface
         * @param identityManager
-        *            The identity manager
+        *              The identity manager
         * @param webOfTrustUpdater
-        *            The WebOfTrust updater
+        *              The WebOfTrust updater
         * @param eventBus
-        *            The event bus
+        *              The event bus
         * @param database
-        *            The database
+        *              The database
         */
        @Inject
        public Core(Configuration configuration, FreenetInterface freenetInterface, IdentityManager identityManager, WebOfTrustUpdater webOfTrustUpdater, EventBus eventBus, Database database) {
@@ -238,20 +233,11 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
        //
 
        /**
-        * Returns the time Sone was started.
-        *
-        * @return The startup time (in milliseconds since Jan 1, 1970 UTC)
-        */
-       public long getStartupTime() {
-               return startupTime;
-       }
-
-       /**
         * Sets the configuration to use. This will automatically save the current
         * configuration to the given configuration.
         *
         * @param configuration
-        *            The new configuration to use
+        *              The new configuration to use
         */
        public void setConfiguration(Configuration configuration) {
                this.configuration = configuration;
@@ -289,17 +275,21 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * Sets the FCP interface to use.
         *
         * @param fcpInterface
-        *            The FCP interface to use
+        *              The FCP interface to use
         */
        public void setFcpInterface(FcpInterface fcpInterface) {
                this.fcpInterface = fcpInterface;
        }
 
+       public Database getDatabase() {
+               return database;
+       }
+
        /**
         * Returns the Sone rescuer for the given local Sone.
         *
         * @param sone
-        *            The local Sone to get the rescuer for
+        *              The local Sone to get the rescuer for
         * @return The Sone rescuer for the given Sone
         */
        public SoneRescuer getSoneRescuer(Sone sone) {
@@ -320,7 +310,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * Returns whether the given Sone is currently locked.
         *
         * @param sone
-        *            The sone to check
+        *              The sone to check
         * @return {@code true} if the Sone is locked, {@code false} if it is not
         */
        public boolean isLocked(Sone sone) {
@@ -329,25 +319,18 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                }
        }
 
-       /**
-        * {@inheritDocs}
-        */
        @Override
        public Collection<Sone> getSones() {
                synchronized (sones) {
-                       return Collections.unmodifiableCollection(sones.values());
+                       return ImmutableSet.copyOf(sones.values());
                }
        }
 
-       /**
-        * 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
-        */
+       @Override
+       public Function<String, Optional<Sone>> getSone() {
+               return database.getSone();
+       }
+
        @Override
        public Optional<Sone> getSone(String id) {
                synchronized (sones) {
@@ -355,19 +338,16 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                }
        }
 
-       /**
-        * {@inheritDocs}
-        */
        @Override
        public Collection<Sone> getLocalSones() {
                synchronized (sones) {
-                       return Collections2.filter(sones.values(), new Predicate<Sone>() {
+                       return from(sones.values()).filter(new Predicate<Sone>() {
 
                                @Override
                                public boolean apply(Sone sone) {
                                        return sone.isLocal();
                                }
-                       });
+                       }).toSet();
                }
        }
 
@@ -375,40 +355,23 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * Returns the local Sone with the given ID, optionally creating a new Sone.
         *
         * @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
+        *              The ID of the Sone
         * @return The Sone with the given ID, or {@code null}
         */
-       public Sone getLocalSone(String id, boolean create) {
-               synchronized (sones) {
-                       Sone sone = sones.get(id);
-                       if ((sone == null) && create) {
-                               sone = new Sone(id, true);
-                               sones.put(id, sone);
-                       }
-                       if ((sone != null) && !sone.isLocal()) {
-                               sone = new Sone(id, true);
-                               sones.put(id, sone);
-                       }
-                       return sone;
-               }
+       public Optional<Sone> getLocalSone(String id) {
+               return from(database.getSone(id).asSet()).firstMatch(LOCAL_SONE_FILTER);
        }
 
-       /**
-        * {@inheritDocs}
-        */
        @Override
        public Collection<Sone> getRemoteSones() {
                synchronized (sones) {
-                       return Collections2.filter(sones.values(), new Predicate<Sone>() {
+                       return from(sones.values()).filter(new Predicate<Sone>() {
 
                                @Override
                                public boolean apply(Sone sone) {
                                        return !sone.isLocal();
                                }
-                       });
+                       }).toSet();
                }
        }
 
@@ -416,30 +379,20 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * 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
+        *              The ID of the remote Sone to get
         * @return The Sone with the given ID
         */
-       public Sone getRemoteSone(String id, boolean create) {
-               synchronized (sones) {
-                       Sone sone = sones.get(id);
-                       if ((sone == null) && create && (id != null) && (id.length() == 43)) {
-                               sone = new Sone(id, false);
-                               sones.put(id, sone);
-                       }
-                       return sone;
-               }
+       public Optional<Sone> getRemoteSone(String id) {
+               return from(database.getSone(id).asSet()).firstMatch(not(LOCAL_SONE_FILTER));
        }
 
        /**
         * Returns whether the given Sone has been modified.
         *
         * @param sone
-        *            The Sone to check for modifications
-        * @return {@code true} if a modification has been detected in the Sone,
-        *         {@code false} otherwise
+        *              The Sone to check for modifications
+        * @return {@code true} if a modification has been detected in the Sone, {@code
+        *         false} otherwise
         */
        public boolean isModifiedSone(Sone sone) {
                return (soneInserters.containsKey(sone)) ? soneInserters.get(sone).isModified() : false;
@@ -449,9 +402,9 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * Returns the time when the given was first followed by any local Sone.
         *
         * @param sone
-        *            The Sone to get the time for
-        * @return The time (in milliseconds since Jan 1, 1970) the Sone has first
-        *         been followed, or {@link Long#MAX_VALUE}
+        *              The Sone to get the time for
+        * @return The time (in milliseconds since Jan 1, 1970) the Sone has first been
+        *         followed, or {@link Long#MAX_VALUE}
         */
        public long getSoneFollowingTime(Sone sone) {
                synchronized (soneFollowingTimes) {
@@ -460,119 +413,10 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
        }
 
        /**
-        * Returns whether the target Sone is trusted by the origin Sone.
-        *
-        * @param origin
-        *            The origin Sone
-        * @param target
-        *            The target Sone
-        * @return {@code true} if the target Sone is trusted by the origin Sone
-        */
-       public boolean isSoneTrusted(Sone origin, Sone target) {
-               checkNotNull(origin, "origin must not be null");
-               checkNotNull(target, "target must not be null");
-               checkArgument(origin.getIdentity() instanceof OwnIdentity, "origin’s identity must be an OwnIdentity");
-               return trustedIdentities.containsEntry(origin.getIdentity(), target.getIdentity());
-       }
-
-       /**
-        * Returns a post builder.
-        *
-        * @return A new post builder
-        */
-       public PostBuilder postBuilder() {
-               return database.newPostBuilder();
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public Optional<Post> getPost(String postId) {
-               return database.getPost(postId);
-       }
-
-       /**
-        * {@inheritDocs}
-        */
-       @Override
-       public Collection<Post> getPosts(String soneId) {
-               return database.getPosts(soneId);
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public Collection<Post> getDirectedPosts(final String recipientId) {
-               checkNotNull(recipientId, "recipient must not be null");
-               return database.getDirectedPosts(recipientId);
-       }
-
-       /**
-        * Returns a post reply builder.
-        *
-        * @return A new post reply builder
-        */
-       public PostReplyBuilder postReplyBuilder() {
-               return database.newPostReplyBuilder();
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public Optional<PostReply> getPostReply(String replyId) {
-               return database.getPostReply(replyId);
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public List<PostReply> getReplies(final String postId) {
-               return database.getReplies(postId);
-       }
-
-       /**
-        * Returns all Sones that have liked the given post.
-        *
-        * @param post
-        *            The post to get the liking Sones for
-        * @return The Sones that like the given post
-        */
-       public Set<Sone> getLikes(Post post) {
-               Set<Sone> sones = new HashSet<Sone>();
-               for (Sone sone : getSones()) {
-                       if (sone.getLikedPostIds().contains(post.getId())) {
-                               sones.add(sone);
-                       }
-               }
-               return sones;
-       }
-
-       /**
-        * Returns all Sones that have liked the given reply.
-        *
-        * @param reply
-        *            The reply to get the liking Sones for
-        * @return The Sones that like the given reply
-        */
-       public Set<Sone> getLikes(PostReply reply) {
-               Set<Sone> sones = new HashSet<Sone>();
-               for (Sone sone : getSones()) {
-                       if (sone.getLikedReplyIds().contains(reply.getId())) {
-                               sones.add(sone);
-                       }
-               }
-               return sones;
-       }
-
-       /**
         * Returns whether the given post is bookmarked.
         *
         * @param post
-        *            The post to check
+        *              The post to check
         * @return {@code true} if the given post is bookmarked, {@code false}
         *         otherwise
         */
@@ -584,9 +428,9 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * 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
+        *              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) {
@@ -603,8 +447,8 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                Set<Post> posts = new HashSet<Post>();
                synchronized (bookmarkedPosts) {
                        for (String bookmarkedPostId : bookmarkedPosts) {
-                               Optional<Post> post = getPost(bookmarkedPostId);
-                               if (!post.isPresent()) {
+                               Optional<Post> post = database.getPost(bookmarkedPostId);
+                               if (post.isPresent()) {
                                        posts.add(post.get());
                                }
                        }
@@ -612,82 +456,21 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                return posts;
        }
 
-       /**
-        * 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);
-       }
-
-       /**
-        * Returns the album with the given ID, optionally creating a new album if
-        * an album with the given ID can not be found.
-        *
-        * @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;
-               }
-       }
-
-       /**
-        * Returns the image with the given ID, creating it if necessary.
-        *
-        * @param imageId
-        *            The ID of the image
-        * @return The image with the given ID
-        */
-       public Image getImage(String imageId) {
-               return getImage(imageId, true);
+       public Optional<Album> getAlbum(String albumId) {
+               return database.getAlbum(albumId);
        }
 
-       /**
-        * Returns the image with the given ID, optionally creating it if it does
-        * not exist.
-        *
-        * @param imageId
-        *            The ID of the image
-        * @param create
-        *            {@code true} to create an image if none exists with the given
-        *            ID
-        * @return The image with the given ID, or {@code null} if none exists and
-        *         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;
-               }
+       public Optional<Image> getImage(String imageId) {
+               return database.getImage(imageId);
        }
 
        /**
         * Returns the temporary image with the given ID.
         *
         * @param imageId
-        *            The ID of the temporary image
-        * @return The temporary image, or {@code null} if there is no temporary
-        *         image with the given ID
+        *              The ID of the temporary image
+        * @return The temporary image, or {@code null} if there is no temporary image
+        *         with the given ID
         */
        public TemporaryImage getTemporaryImage(String imageId) {
                synchronized (temporaryImages) {
@@ -700,12 +483,11 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
        //
 
        /**
-        * Locks the given Sone. A locked Sone will not be inserted by
-        * {@link SoneInserter} until it is {@link #unlockSone(Sone) unlocked}
-        * again.
+        * Locks the given Sone. A locked Sone will not be inserted by {@link
+        * SoneInserter} until it is {@link #unlockSone(Sone) unlocked} again.
         *
         * @param sone
-        *            The sone to lock
+        *              The sone to lock
         */
        public void lockSone(Sone sone) {
                synchronized (lockedSones) {
@@ -718,9 +500,9 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
        /**
         * Unlocks the given Sone.
         *
-        * @see #lockSone(Sone)
         * @param sone
-        *            The sone to unlock
+        *              The sone to unlock
+        * @see #lockSone(Sone)
         */
        public void unlockSone(Sone sone) {
                synchronized (lockedSones) {
@@ -734,7 +516,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * Adds a local Sone from the given own identity.
         *
         * @param ownIdentity
-        *            The own identity to create a Sone from
+        *              The own identity to create a Sone from
         * @return The added (or already existing) Sone
         */
        public Sone addLocalSone(OwnIdentity ownIdentity) {
@@ -742,16 +524,11 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                        logger.log(Level.WARNING, "Given OwnIdentity is null!");
                        return null;
                }
+               logger.info(String.format("Adding Sone from OwnIdentity: %s", ownIdentity));
                synchronized (sones) {
                        final Sone sone;
-                       try {
-                               sone = getLocalSone(ownIdentity.getId(), true).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 = database.newSoneBuilder().by(ownIdentity.getId()).local().using(new Client("Sone", SonePlugin.VERSION.toString())).build(Optional.<SoneCreated>absent());
+                       sone.modify().setLatestEdition(Numbers.safeParseLong(ownIdentity.getProperty("Sone.LatestEdition"), (long) 0)).update();
                        sone.setKnown(true);
                        /* TODO - load posts ’n stuff */
                        sones.put(ownIdentity.getId(), sone);
@@ -768,7 +545,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * Creates a new Sone for the given own identity.
         *
         * @param ownIdentity
-        *            The own identity to create a Sone for
+        *              The own identity to create a Sone for
         * @return The created Sone
         */
        public Sone createSone(OwnIdentity ownIdentity) {
@@ -793,7 +570,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * Adds the Sone of the given identity.
         *
         * @param identity
-        *            The identity whose Sone to add
+        *              The identity whose Sone to add
         * @return The added or already existing Sone
         */
        public Sone addRemoteSone(Identity identity) {
@@ -802,14 +579,13 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                        return null;
                }
                synchronized (sones) {
-                       final Sone sone = getRemoteSone(identity.getId(), true);
-                       if (sone.isLocal()) {
-                               return sone;
-                       }
-                       sone.setIdentity(identity);
-                       boolean newSone = sone.getRequestUri() == null;
-                       sone.setRequestUri(SoneUri.create(identity.getRequestUri()));
-                       sone.setLatestEdition(Numbers.safeParseLong(identity.getProperty("Sone.LatestEdition"), (long) 0));
+                       Optional<Sone> existingSone = database.getSone(identity.getId());
+                       if (existingSone.isPresent() && existingSone.get().isLocal()) {
+                               return existingSone.get();
+                       }
+                       boolean newSone = !existingSone.isPresent();
+                       final Sone sone = newSone ? database.newSoneBuilder().by(identity.getId()).using(new Client("Sone", SonePlugin.VERSION.toString())).build(Optional.<SoneCreated>absent()) : existingSone.get();
+                       sone.modify().setLatestEdition(Numbers.safeParseLong(identity.getProperty("Sone.LatestEdition"), (long) 0)).update();
                        if (newSone) {
                                synchronized (knownSones) {
                                        newSone = !knownSones.contains(sone.getId());
@@ -830,7 +606,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                                @Override
                                @SuppressWarnings("synthetic-access")
                                public void run() {
-                                       soneDownloader.fetchSone(sone, sone.getRequestUri());
+                                       soneDownloader.fetchSone(sone, TO_FREENET_URI.apply(sone));
                                }
 
                        });
@@ -842,9 +618,9 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * Lets the given local Sone follow the Sone with the given ID.
         *
         * @param sone
-        *            The local Sone that should follow another Sone
+        *              The local Sone that should follow another Sone
         * @param soneId
-        *            The ID of the Sone to follow
+        *              The ID of the Sone to follow
         */
        public void followSone(Sone sone, String soneId) {
                checkNotNull(sone, "sone must not be null");
@@ -865,7 +641,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                                }
                                for (PostReply reply : followedSone.get().getReplies()) {
                                        if (reply.getTime() < now) {
-                                               markReplyKnown(reply);
+                                               reply.modify().setKnown().update(Optional.<ReplyUpdated<PostReply>>absent());
                                        }
                                }
                        }
@@ -877,9 +653,9 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * Lets the given local Sone unfollow the Sone with the given ID.
         *
         * @param sone
-        *            The local Sone that should unfollow another Sone
+        *              The local Sone that should unfollow another Sone
         * @param soneId
-        *            The ID of the Sone being unfollowed
+        *              The ID of the Sone being unfollowed
         */
        public void unfollowSone(Sone sone, String soneId) {
                checkNotNull(sone, "sone must not be null");
@@ -901,11 +677,11 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * Sets the trust value of the given origin Sone for the target Sone.
         *
         * @param origin
-        *            The origin Sone
+        *              The origin Sone
         * @param target
-        *            The target Sone
+        *              The target Sone
         * @param trustValue
-        *            The trust value (from {@code -100} to {@code 100})
+        *              The trust value (from {@code -100} to {@code 100})
         */
        public void setTrust(Sone origin, Sone target, int trustValue) {
                checkNotNull(origin, "origin must not be null");
@@ -919,9 +695,9 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * Removes any trust assignment for the given target Sone.
         *
         * @param origin
-        *            The trust origin
+        *              The trust origin
         * @param target
-        *            The trust target
+        *              The trust target
         */
        public void removeTrust(Sone origin, Sone target) {
                checkNotNull(origin, "origin must not be null");
@@ -934,9 +710,9 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * Assigns the configured positive trust value for the given target.
         *
         * @param origin
-        *            The trust origin
+        *              The trust origin
         * @param target
-        *            The trust target
+        *              The trust target
         */
        public void trustSone(Sone origin, Sone target) {
                setTrust(origin, target, preferences.getPositiveTrust());
@@ -946,9 +722,9 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * Assigns the configured negative trust value for the given target.
         *
         * @param origin
-        *            The trust origin
+        *              The trust origin
         * @param target
-        *            The trust target
+        *              The trust target
         */
        public void distrustSone(Sone origin, Sone target) {
                setTrust(origin, target, preferences.getNegativeTrust());
@@ -958,9 +734,9 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * Removes the trust assignment for the given target.
         *
         * @param origin
-        *            The trust origin
+        *              The trust origin
         * @param target
-        *            The trust target
+        *              The trust target
         */
        public void untrustSone(Sone origin, Sone target) {
                removeTrust(origin, target);
@@ -970,7 +746,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * Updates the stored Sone with the given Sone.
         *
         * @param sone
-        *            The updated Sone
+        *              The updated Sone
         */
        public void updateSone(Sone sone) {
                updateSone(sone, false);
@@ -978,14 +754,14 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
 
        /**
         * Updates the stored Sone with the given Sone. If {@code soneRescueMode} is
-        * {@code true}, an older Sone than the current Sone can be given to restore
-        * an old state.
+        * {@code true}, an older Sone than the current Sone can be given to restore an
+        * old state.
         *
         * @param sone
-        *            The Sone to update
+        *              The Sone to update
         * @param soneRescueMode
-        *            {@code true} if the stored Sone should be updated regardless
-        *            of the age of the given Sone
+        *              {@code true} if the stored Sone should be updated regardless of the age of
+        *              the given Sone
         */
        public void updateSone(Sone sone, boolean soneRescueMode) {
                Optional<Sone> storedSone = getSone(sone.getId());
@@ -1027,42 +803,44 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                                        continue;
                                }
                                if (reply.getTime() < getSoneFollowingTime(sone)) {
-                                       reply.setKnown(true);
+                                       reply.modify().setKnown().update(Optional.<ReplyUpdated<PostReply>>absent());
                                } else if (!reply.isKnown()) {
                                        eventBus.post(new NewPostReplyFoundEvent(reply));
                                }
                        }
                        database.storePostReplies(sone, sone.getReplies());
-                       synchronized (albums) {
-                               synchronized (images) {
-                                       for (Album album : storedSone.get().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);
-                                               }
-                                       }
+                       for (Album album : storedSone.get().getRootAlbum().getAlbums()) {
+                               database.removeAlbum(album);
+                               for (Image image : album.getImages()) {
+                                       database.removeImage(image);
+                               }
+                       }
+                       for (Album album : sone.getRootAlbum().getAlbums()) {
+                               database.storeAlbum(album);
+                               for (Image image : album.getImages()) {
+                                       database.storeImage(image);
                                }
                        }
                        synchronized (sones) {
                                sone.setOptions(storedSone.get().getOptions());
+                               sone.setKnown(storedSone.get().isKnown());
+                               sone.setStatus((sone.getTime() == 0) ? SoneStatus.unknown : SoneStatus.idle);
+                               if (sone.isLocal()) {
+                                       soneInserters.get(storedSone.get()).setSone(sone);
+                                       touchConfiguration();
+                               }
                                sones.put(sone.getId(), sone);
                        }
                }
        }
 
        /**
-        * Deletes the given Sone. This will remove the Sone from the
-        * {@link #getLocalSones() local Sones}, stop its {@link SoneInserter} and
-        * remove the context from its identity.
+        * Deletes the given Sone. This will remove the Sone from the {@link
+        * #getLocalSones() local Sones}, stop its {@link SoneInserter} and remove the
+        * context from its identity.
         *
         * @param sone
-        *            The Sone to delete
+        *              The Sone to delete
         */
        public void deleteSone(Sone sone) {
                if (!(sone.getIdentity() instanceof OwnIdentity)) {
@@ -1092,7 +870,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * known} before, a {@link MarkSoneKnownEvent} is fired.
         *
         * @param sone
-        *            The Sone to mark as known
+        *              The Sone to mark as known
         */
        public void markSoneKnown(Sone sone) {
                if (!sone.isKnown()) {
@@ -1110,13 +888,14 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * encountered, loading is aborted and the given Sone is not changed.
         *
         * @param sone
-        *            The Sone to load and update
+        *              The Sone to load and update
         */
        public void loadSone(Sone sone) {
                if (!sone.isLocal()) {
                        logger.log(Level.FINE, String.format("Tried to load non-local Sone: %s", sone));
                        return;
                }
+               logger.info(String.format("Loading local Sone: %s", sone));
 
                /* initialize options. */
                sone.getOptions().addBooleanOption("AutoFollow", new DefaultOption<Boolean>(false));
@@ -1137,12 +916,14 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
 
                /* 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));
+               String firstName = configuration.getStringValue(sonePrefix + "/Profile/FirstName").getValue(null);
+               String middleName = configuration.getStringValue(sonePrefix + "/Profile/MiddleName").getValue(null);
+               String lastName = configuration.getStringValue(sonePrefix + "/Profile/LastName").getValue(null);
+               profile.modify().setFirstName(firstName).setMiddleName(middleName).setLastName(lastName).update();
+               Integer birthDay = configuration.getIntValue(sonePrefix + "/Profile/BirthDay").getValue(null);
+               Integer birthMonth = configuration.getIntValue(sonePrefix + "/Profile/BirthMonth").getValue(null);
+               Integer birthYear = configuration.getIntValue(sonePrefix + "/Profile/BirthYear").getValue(null);
+               profile.modify().setBirthYear(birthYear).setBirthMonth(birthMonth).setBirthDay(birthDay).update();
 
                /* load profile fields. */
                while (true) {
@@ -1152,7 +933,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                                break;
                        }
                        String fieldValue = configuration.getStringValue(fieldPrefix + "/Value").getValue("");
-                       profile.addField(fieldName).setValue(fieldValue);
+                       profile.setField(profile.addField(fieldName), fieldValue);
                }
 
                /* load posts. */
@@ -1170,11 +951,11 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                                logger.log(Level.WARNING, "Invalid post found, aborting load!");
                                return;
                        }
-                       PostBuilder postBuilder = postBuilder().withId(postId).from(sone.getId()).withTime(postTime).withText(postText);
+                       PostBuilder postBuilder = sone.newPostBuilder().withId(postId).withTime(postTime).withText(postText);
                        if ((postRecipientId != null) && (postRecipientId.length() == 43)) {
-                               postBuilder.to(postRecipientId);
+                               postBuilder.to(of(postRecipientId));
                        }
-                       posts.add(postBuilder.build());
+                       posts.add(postBuilder.build(Optional.<PostCreated>absent()));
                }
 
                /* load replies. */
@@ -1192,8 +973,8 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                                logger.log(Level.WARNING, "Invalid reply found, aborting load!");
                                return;
                        }
-                       PostReplyBuilder postReplyBuilder = postReplyBuilder().withId(replyId).from(sone.getId()).to(postId).withTime(replyTime).withText(replyText);
-                       replies.add(postReplyBuilder.build());
+                       PostReplyBuilder postReplyBuilder = sone.newPostReplyBuilder(postId).withId(replyId).withTime(replyTime).withText(replyText);
+                       replies.add(postReplyBuilder.build(Optional.<PostReplyCreated>absent()));
                }
 
                /* load post likes. */
@@ -1227,7 +1008,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                }
 
                /* load albums. */
-               List<Album> topLevelAlbums = new ArrayList<Album>();
+               Map<String, Album> albums = Maps.newHashMap();
                int albumCounter = 0;
                while (true) {
                        String albumPrefix = sonePrefix + "/Albums/" + albumCounter++;
@@ -1243,19 +1024,12 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                                logger.log(Level.WARNING, "Invalid album found, aborting load!");
                                return;
                        }
-                       Album album = getAlbum(albumId).setSone(sone).setTitle(albumTitle).setDescription(albumDescription).setAlbumImage(albumImageId);
+                       Album parentAlbum = sone.getRootAlbum();
                        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);
-                               }
+                               parentAlbum = albums.get(albumParentId);
                        }
+                       Album album = parentAlbum.newAlbumBuilder().withId(albumId).build().modify().setTitle(albumTitle).setDescription(albumDescription).setAlbumImage(albumImageId).update();
+                       albums.put(album.getId(), album);
                }
 
                /* load images. */
@@ -1277,20 +1051,18 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                                logger.log(Level.WARNING, "Invalid image found, aborting load!");
                                return;
                        }
-                       Album album = getAlbum(albumId, false);
+                       Album album = albums.get(albumId);
                        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);
+                       album.newImageBuilder().withId(imageId).created(creationTime).at(key).sized(width, height).build(Optional.<ImageCreated>absent()).modify().setTitle(title).setDescription(description).update();
                }
 
                /* load avatar. */
                String avatarId = configuration.getStringValue(sonePrefix + "/Profile/Avatar").getValue(null);
                if (avatarId != null) {
-                       profile.setAvatar(getImage(avatarId, false));
+                       profile.setAvatar(getImage(avatarId).transform(GET_ID));
                }
 
                /* load options. */
@@ -1299,7 +1071,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                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().<ShowCustomAvatars>getEnumOption("ShowCustomAvatars").set(ShowCustomAvatars.valueOf(configuration.getStringValue(sonePrefix + "/Options/ShowCustomAvatars").getValue(ShowCustomAvatars.NEVER.name())));
 
                /* if we’re still here, Sone was loaded successfully. */
                synchronized (sone) {
@@ -1312,7 +1084,9 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                        for (String friendId : friends) {
                                followSone(sone, friendId);
                        }
-                       sone.setAlbums(topLevelAlbums);
+                       for (Album album : sone.getRootAlbum().getAlbums()) {
+                               album.remove();
+                       }
                        soneInserters.get(sone).setLastInsertFingerprint(lastInsertFingerprint);
                }
                synchronized (knownSones) {
@@ -1326,103 +1100,17 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                }
                database.storePostReplies(sone, replies);
                for (PostReply reply : replies) {
-                       reply.setKnown(true);
+                       reply.modify().setKnown().update(Optional.<ReplyUpdated<PostReply>>absent());
                }
-       }
-
-       /**
-        * 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);
-       }
-
-       /**
-        * 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 text
-        *            The text of the post
-        * @return The created post
-        */
-       public Post createPost(Sone sone, Optional<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, Optional<Sone> recipient, long time, 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;
-               }
-               PostBuilder postBuilder = database.newPostBuilder();
-               postBuilder.from(sone.getId()).randomId().withTime(time).withText(text.trim());
-               if (recipient.isPresent()) {
-                       postBuilder.to(recipient.get().getId());
-               }
-               final Post post = postBuilder.build();
-               database.storePost(post);
-               eventBus.post(new NewPostFoundEvent(post));
-               sone.addPost(post);
-               touchConfiguration();
-               localElementTicker.schedule(new Runnable() {
-
-                       /**
-                        * {@inheritDoc}
-                        */
-                       @Override
-                       public void run() {
-                               markPostKnown(post);
-                       }
-               }, 10, TimeUnit.SECONDS);
-               return post;
+               logger.info(String.format("Sone loaded successfully: %s", sone));
        }
 
        /**
         * Deletes the given post.
         *
         * @param post
-        *            The post to delete
+        *              The post to delete
         */
        public void deletePost(Post post) {
                if (!post.getSone().isLocal()) {
@@ -1440,32 +1128,22 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * (according to {@link Post#isKnown()}).
         *
         * @param post
-        *            The post to mark as known
+        *              The post to mark as known
         */
        public void markPostKnown(Post post) {
                post.setKnown(true);
                eventBus.post(new MarkPostKnownEvent(post));
                touchConfiguration();
-               for (PostReply reply : getReplies(post.getId())) {
-                       markReplyKnown(reply);
+               for (PostReply reply : post.getReplies()) {
+                       reply.modify().setKnown().update(postReplyUpdated());
                }
        }
 
        /**
-        * 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
+        *              The ID of the post to bookmark
         */
        public void bookmarkPost(String id) {
                synchronized (bookmarkedPosts) {
@@ -1477,7 +1155,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * Removes the given post from the bookmarks.
         *
         * @param post
-        *            The post to unbookmark
+        *              The post to unbookmark
         */
        public void unbookmark(Post post) {
                unbookmarkPost(post.getId());
@@ -1487,7 +1165,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * Removes the post with the given ID from the bookmarks.
         *
         * @param id
-        *            The ID of the post to unbookmark
+        *              The ID of the post to unbookmark
         */
        public void unbookmarkPost(String id) {
                synchronized (bookmarkedPosts) {
@@ -1496,48 +1174,10 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
        }
 
        /**
-        * Creates a new reply.
-        *
-        * @param sone
-        *            The Sone that creates the reply
-        * @param post
-        *            The post that this reply refers to
-        * @param text
-        *            The text of the reply
-        * @return The created reply
-        */
-       public PostReply createReply(Sone sone, Post post, 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 reply for non-local Sone: %s", sone));
-                       return null;
-               }
-               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.schedule(new Runnable() {
-
-                       /**
-                        * {@inheritDoc}
-                        */
-                       @Override
-                       public void run() {
-                               markReplyKnown(reply);
-                       }
-               }, 10, TimeUnit.SECONDS);
-               return reply;
-       }
-
-       /**
         * Deletes the given reply.
         *
         * @param reply
-        *            The reply to delete
+        *              The reply to delete
         */
        public void deleteReply(PostReply reply) {
                Sone sone = reply.getSone();
@@ -1545,84 +1185,8 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                        logger.log(Level.FINE, String.format("Tried to delete non-local reply: %s", reply));
                        return;
                }
+               postReplyUpdated().get().replyUpdated(reply);
                database.removePostReply(reply);
-               markReplyKnown(reply);
-               sone.removeReply(reply);
-               touchConfiguration();
-       }
-
-       /**
-        * Marks the given reply as known, if it is currently not a known reply
-        * (according to {@link Reply#isKnown()}).
-        *
-        * @param reply
-        *            The reply to mark as known
-        */
-       public void markReplyKnown(PostReply reply) {
-               boolean previouslyKnown = reply.isKnown();
-               reply.setKnown(true);
-               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
-        *            The Sone to create the album for
-        * @param parent
-        *            The parent of the album (may be {@code null} to create a
-        *            top-level album)
-        * @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);
-               }
-               return album;
-       }
-
-       /**
-        * Deletes the given album. The owner of the album has to be a local Sone,
-        * and the album has to be {@link Album#isEmpty() empty} to be deleted.
-        *
-        * @param album
-        *            The album to remove
-        */
-       public void deleteAlbum(Album album) {
-               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());
-               }
                touchConfiguration();
        }
 
@@ -1630,11 +1194,11 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * Creates a new image.
         *
         * @param sone
-        *            The Sone creating the image
+        *              The Sone creating the image
         * @param album
-        *            The album the image will be inserted into
+        *              The album the image will be inserted into
         * @param temporaryImage
-        *            The temporary image to create the image from
+        *              The temporary image to create the image from
         * @return The newly created image
         */
        public Image createImage(Sone sone, Album album, TemporaryImage temporaryImage) {
@@ -1643,31 +1207,24 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                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 = new Image(temporaryImage.getId()).setSone(sone).setCreationTime(System.currentTimeMillis());
-               album.addImage(image);
-               synchronized (images) {
-                       images.put(image.getId(), image);
-               }
+               Image image = album.newImageBuilder().withId(temporaryImage.getId()).sized(temporaryImage.getWidth(), temporaryImage.getHeight()).build(imageCreated());
                imageInserter.insertImage(temporaryImage, image);
                return image;
        }
 
        /**
-        * Deletes the given image. This method will also delete a matching
-        * temporary image.
+        * Deletes the given image. This method will also delete a matching temporary
+        * image.
         *
-        * @see #deleteTemporaryImage(TemporaryImage)
         * @param image
-        *            The image to delete
+        *              The image to delete
+        * @see #deleteTemporaryImage(String)
         */
        public void deleteImage(Image image) {
                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());
-               }
+               image.remove();
                touchConfiguration();
        }
 
@@ -1675,14 +1232,13 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * Creates a new temporary image.
         *
         * @param mimeType
-        *            The MIME type of the temporary image
+        *              The MIME type of the temporary image
         * @param imageData
-        *            The encoded data of the image
+        *              The encoded data of the image
         * @return The temporary image
         */
-       public TemporaryImage createTemporaryImage(String mimeType, byte[] imageData) {
-               TemporaryImage temporaryImage = new TemporaryImage();
-               temporaryImage.setMimeType(mimeType).setImageData(imageData);
+       public TemporaryImage createTemporaryImage(String mimeType, byte[] imageData, int width, int height) {
+               TemporaryImage temporaryImage = new TemporaryImage(mimeType, imageData, width, height);
                synchronized (temporaryImages) {
                        temporaryImages.put(temporaryImage.getId(), temporaryImage);
                }
@@ -1690,37 +1246,25 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
        }
 
        /**
-        * Deletes the given temporary image.
-        *
-        * @param temporaryImage
-        *            The temporary image to delete
-        */
-       public void deleteTemporaryImage(TemporaryImage temporaryImage) {
-               checkNotNull(temporaryImage, "temporaryImage must not be null");
-               deleteTemporaryImage(temporaryImage.getId());
-       }
-
-       /**
         * Deletes the temporary image with the given ID.
         *
         * @param imageId
-        *            The ID of the temporary image to delete
+        *              The ID of the temporary image to delete
         */
        public void deleteTemporaryImage(String imageId) {
                checkNotNull(imageId, "imageId must not be null");
                synchronized (temporaryImages) {
                        temporaryImages.remove(imageId);
                }
-               Image image = getImage(imageId, false);
-               if (image != null) {
-                       imageInserter.cancelImageInsert(image);
+               Optional<Image> image = getImage(imageId);
+               if (image.isPresent()) {
+                       imageInserter.cancelImageInsert(image.get());
                }
        }
 
        /**
-        * Notifies the core that the configuration, either of the core or of a
-        * single local Sone, has changed, and that the configuration should be
-        * saved.
+        * Notifies the core that the configuration, either of the core or of a single
+        * local Sone, has changed, and that the configuration should be saved.
         */
        public void touchConfiguration() {
                lastConfigurationUpdate = System.currentTimeMillis();
@@ -1730,9 +1274,6 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
        // SERVICE METHODS
        //
 
-       /**
-        * Starts the core.
-        */
        @Override
        public void serviceStart() {
                loadConfiguration();
@@ -1743,9 +1284,6 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                database.start();
        }
 
-       /**
-        * {@inheritDoc}
-        */
        @Override
        public void serviceRun() {
                long lastSaved = System.currentTimeMillis();
@@ -1762,9 +1300,6 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                }
        }
 
-       /**
-        * Stops the core.
-        */
        @Override
        public void serviceStop() {
                localElementTicker.shutdownNow();
@@ -1792,7 +1327,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * Sone, such as the friends list and similar, private options.
         *
         * @param sone
-        *            The Sone to save
+        *              The Sone to save
         */
        private synchronized void saveSone(Sone sone) {
                if (!sone.isLocal()) {
@@ -1874,7 +1409,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                        configuration.getStringValue(sonePrefix + "/Friends/" + friendCounter + "/ID").setValue(null);
 
                        /* save albums. first, collect in a flat structure, top-level first. */
-                       List<Album> albums = FluentIterable.from(sone.getAlbums()).transformAndConcat(Album.FLATTENER).toList();
+                       List<Album> albums = from(sone.getRootAlbum().getAlbums()).transformAndConcat(Album.FLATTENER).toList();
 
                        int albumCounter = 0;
                        for (Album album : albums) {
@@ -1882,8 +1417,8 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                                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 + "/AlbumImage").setValue(album.getAlbumImage() == null ? null : album.getAlbumImage().getId());
+                               configuration.getStringValue(albumPrefix + "/Parent").setValue(album.getParent().equals(sone.getRootAlbum()) ? null : album.getParent().getId());
+                               configuration.getStringValue(albumPrefix + "/AlbumImage").setValue(album.getAlbumImage().transform(GET_ID).orNull());
                        }
                        configuration.getStringValue(sonePrefix + "/Albums/" + albumCounter + "/ID").setValue(null);
 
@@ -1913,7 +1448,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                        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.getStringValue(sonePrefix + "/Options/ShowCustomAvatars").setValue(sone.getOptions().<ShowCustomAvatars>getEnumOption("ShowCustomAvatars").get().name());
 
                        configuration.save();
 
@@ -1925,9 +1460,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                }
        }
 
-       /**
-        * Saves the current options.
-        */
+       /** Saves the current options. */
        private void saveConfiguration() {
                synchronized (configuration) {
                        if (storingConfiguration) {
@@ -1998,9 +1531,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                }
        }
 
-       /**
-        * Loads the configuration.
-        */
+       /** Loads the configuration. */
        private void loadConfiguration() {
                /* create options. */
                options.addIntegerOption("InsertionDelay", new DefaultOption<Integer>(60, new IntegerRangePredicate(0, Integer.MAX_VALUE), new OptionWatcher<Integer>() {
@@ -2013,8 +1544,8 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                }));
                options.addIntegerOption("PostsPerPage", new DefaultOption<Integer>(10, new IntegerRangePredicate(1, Integer.MAX_VALUE)));
                options.addIntegerOption("ImagesPerPage", new DefaultOption<Integer>(9, new IntegerRangePredicate(1, Integer.MAX_VALUE)));
-               options.addIntegerOption("CharactersPerPost", new DefaultOption<Integer>(400, Predicates.<Integer> or(new IntegerRangePredicate(50, Integer.MAX_VALUE), Predicates.equalTo(-1))));
-               options.addIntegerOption("PostCutOffLength", new DefaultOption<Integer>(200, Predicates.<Integer> or(new IntegerRangePredicate(50, Integer.MAX_VALUE), Predicates.equalTo(-1))));
+               options.addIntegerOption("CharactersPerPost", new DefaultOption<Integer>(400, Predicates.<Integer>or(new IntegerRangePredicate(50, Integer.MAX_VALUE), Predicates.equalTo(-1))));
+               options.addIntegerOption("PostCutOffLength", new DefaultOption<Integer>(200, Predicates.<Integer>or(new IntegerRangePredicate(50, Integer.MAX_VALUE), Predicates.equalTo(-1))));
                options.addBooleanOption("RequireFullAccess", new DefaultOption<Boolean>(false));
                options.addIntegerOption("PositiveTrust", new DefaultOption<Integer>(75, new IntegerRangePredicate(0, 100)));
                options.addIntegerOption("NegativeTrust", new DefaultOption<Integer>(-25, new IntegerRangePredicate(-100, 100)));
@@ -2090,11 +1621,11 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
        }
 
        /**
-        * Loads an {@link Integer} configuration value for the option with the
-        * given name, logging validation failures.
+        * Loads an {@link Integer} configuration value for the option with the given
+        * name, logging validation failures.
         *
         * @param optionName
-        *            The name of the option to load
+        *              The name of the option to load
         */
        private void loadConfigurationValue(String optionName) {
                try {
@@ -2108,13 +1639,14 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * Notifies the core that a new {@link OwnIdentity} was added.
         *
         * @param ownIdentityAddedEvent
-        *            The event
+        *              The event
         */
        @Subscribe
        public void ownIdentityAdded(OwnIdentityAddedEvent ownIdentityAddedEvent) {
                OwnIdentity ownIdentity = ownIdentityAddedEvent.ownIdentity();
                logger.log(Level.FINEST, String.format("Adding OwnIdentity: %s", ownIdentity));
                if (ownIdentity.hasContext("Sone")) {
+                       database.storeIdentity(ownIdentity);
                        addLocalSone(ownIdentity);
                }
        }
@@ -2123,7 +1655,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * Notifies the core that an {@link OwnIdentity} was removed.
         *
         * @param ownIdentityRemovedEvent
-        *            The event
+        *              The event
         */
        @Subscribe
        public void ownIdentityRemoved(OwnIdentityRemovedEvent ownIdentityRemovedEvent) {
@@ -2136,13 +1668,14 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * Notifies the core that a new {@link Identity} was added.
         *
         * @param identityAddedEvent
-        *            The event
+        *              The event
         */
        @Subscribe
        public void identityAdded(IdentityAddedEvent identityAddedEvent) {
                Identity identity = identityAddedEvent.identity();
                logger.log(Level.FINEST, String.format("Adding Identity: %s", identity));
                trustedIdentities.put(identityAddedEvent.ownIdentity(), identity);
+               database.storeIdentity(identity);
                addRemoteSone(identity);
        }
 
@@ -2150,21 +1683,21 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * Notifies the core that an {@link Identity} was updated.
         *
         * @param identityUpdatedEvent
-        *            The event
+        *              The event
         */
        @Subscribe
        public void identityUpdated(IdentityUpdatedEvent identityUpdatedEvent) {
                final Identity identity = identityUpdatedEvent.identity();
+               database.storeIdentity(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);
+                               Optional<Sone> sone = getRemoteSone(identity.getId());
+                               sone.get().modify().setLatestEdition(Numbers.safeParseLong(identity.getProperty("Sone.LatestEdition"), sone.get().getLatestEdition())).update();
+                               soneDownloader.addSone(sone.get());
+                               soneDownloader.fetchSone(sone.get());
                        }
                });
        }
@@ -2173,7 +1706,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * Notifies the core that an {@link Identity} was removed.
         *
         * @param identityRemovedEvent
-        *            The event
+        *              The event
         */
        @Subscribe
        public void identityRemoved(IdentityRemovedEvent identityRemovedEvent) {
@@ -2216,14 +1749,76 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * Deletes the temporary image.
         *
         * @param imageInsertFinishedEvent
-        *            The event
+        *              The event
         */
        @Subscribe
        public void imageInsertFinished(ImageInsertFinishedEvent imageInsertFinishedEvent) {
                logger.log(Level.WARNING, String.format("Image insert finished for %s: %s", imageInsertFinishedEvent.image(), imageInsertFinishedEvent.resultingUri()));
-               imageInsertFinishedEvent.image().setKey(imageInsertFinishedEvent.resultingUri().toString());
+               imageInsertFinishedEvent.image().modify().setKey(imageInsertFinishedEvent.resultingUri().toString()).update();
                deleteTemporaryImage(imageInsertFinishedEvent.image().getId());
                touchConfiguration();
        }
 
+       public Optional<PostCreated> postCreated() {
+               return Optional.<PostCreated>of(new PostCreated() {
+                       @Override
+                       public void postCreated(final Post post) {
+                               if (post.isKnown()) {
+                                       return;
+                               }
+                               eventBus.post(new NewPostFoundEvent(post));
+                               if (post.getSone().isLocal()) {
+                                       localElementTicker.schedule(new Runnable() {
+                                               @Override
+                                               public void run() {
+                                                       markPostKnown(post);
+                                               }
+                                       }, 10, TimeUnit.SECONDS);
+                               }
+                       }
+               });
+       }
+
+       public Optional<PostReplyCreated> postReplyCreated() {
+               return Optional.<PostReplyCreated>of(new PostReplyCreated() {
+                       @Override
+                       public void postReplyCreated(final PostReply postReply) {
+                               if (postReply.isKnown()) {
+                                       return;
+                               }
+                               eventBus.post(new NewPostReplyFoundEvent(postReply));
+                               if (postReply.getSone().isLocal()) {
+                                       localElementTicker.schedule(new Runnable() {
+
+                                               /**
+                                                * {@inheritDoc}
+                                                */
+                                               @Override
+                                               public void run() {
+                                                       postReplyUpdated().get().replyUpdated(postReply);
+                                               }
+                                       }, 10, TimeUnit.SECONDS);
+                               }
+                       }
+               });
+       }
+
+       public Optional<ReplyUpdated<PostReply>> postReplyUpdated() {
+               return Optional.<ReplyUpdated<PostReply>>of(new ReplyUpdated<PostReply>() {
+                       @Override
+                       public void replyUpdated(PostReply reply) {
+                               eventBus.post(new MarkPostReplyKnownEvent(reply));
+                       }
+               });
+       }
+
+       public Optional<ImageCreated> imageCreated() {
+               return Optional.<ImageCreated>of(new ImageCreated() {
+                       @Override
+                       public void imageCreated(Image image) {
+                               /* nothing happens here yet. */
+                       }
+               });
+       }
+
 }