Simplify album removal.
[Sone.git] / src / main / java / net / pterodactylus / sone / core / Core.java
index 60c2506..e61a58f 100644 (file)
@@ -21,7 +21,6 @@ import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
 
 import java.net.MalformedURLException;
-import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -62,6 +61,7 @@ import net.pterodactylus.sone.data.Reply;
 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.SoneImpl;
 import net.pterodactylus.sone.data.TemporaryImage;
 import net.pterodactylus.sone.database.Database;
 import net.pterodactylus.sone.database.DatabaseException;
@@ -89,20 +89,21 @@ import net.pterodactylus.util.number.Numbers;
 import net.pterodactylus.util.service.AbstractService;
 import net.pterodactylus.util.thread.NamedThreadFactory;
 
+import freenet.keys.FreenetURI;
+
 import com.google.common.base.Optional;
 import com.google.common.base.Predicate;
 import com.google.common.base.Predicates;
 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.
  *
@@ -187,12 +188,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>();
 
@@ -206,17 +201,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) {
@@ -250,7 +245,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * 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;
@@ -288,7 +283,7 @@ 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;
@@ -298,7 +293,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * 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) {
@@ -319,7 +314,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) {
@@ -328,9 +323,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                }
        }
 
-       /**
-        * {@inheritDocs}
-        */
+       /** {@inheritDocs} */
        @Override
        public Collection<Sone> getSones() {
                synchronized (sones) {
@@ -343,7 +336,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * remote.
         *
         * @param id
-        *            The ID of the Sone to get
+        *              The ID of the Sone to get
         * @return The Sone with the given ID, or {@code null} if there is no such
         *         Sone
         */
@@ -354,9 +347,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                }
        }
 
-       /**
-        * {@inheritDocs}
-        */
+       /** {@inheritDocs} */
        @Override
        public Collection<Sone> getLocalSones() {
                synchronized (sones) {
@@ -374,30 +365,28 @@ 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
+        *              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
+        *              {@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 (sones) {
                        Sone sone = sones.get(id);
                        if ((sone == null) && create) {
-                               sone = new Sone(id, true);
+                               sone = new SoneImpl(id, true);
                                sones.put(id, sone);
                        }
                        if ((sone != null) && !sone.isLocal()) {
-                               sone = new Sone(id, true);
+                               sone = new SoneImpl(id, true);
                                sones.put(id, sone);
                        }
                        return sone;
                }
        }
 
-       /**
-        * {@inheritDocs}
-        */
+       /** {@inheritDocs} */
        @Override
        public Collection<Sone> getRemoteSones() {
                synchronized (sones) {
@@ -415,17 +404,17 @@ 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
+        *              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
+        *              {@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 (sones) {
                        Sone sone = sones.get(id);
                        if ((sone == null) && create && (id != null) && (id.length() == 43)) {
-                               sone = new Sone(id, false);
+                               sone = new SoneImpl(id, false);
                                sones.put(id, sone);
                        }
                        return sone;
@@ -436,9 +425,9 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * 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;
@@ -448,9 +437,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) {
@@ -462,9 +451,9 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * Returns whether the target Sone is trusted by the origin Sone.
         *
         * @param origin
-        *            The origin Sone
+        *              The origin Sone
         * @param target
-        *            The target Sone
+        *              The target Sone
         * @return {@code true} if the target Sone is trusted by the origin Sone
         */
        public boolean isSoneTrusted(Sone origin, Sone target) {
@@ -483,25 +472,19 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                return database.newPostBuilder();
        }
 
-       /**
-        * {@inheritDoc}
-        */
+       /** {@inheritDoc} */
        @Override
        public Optional<Post> getPost(String postId) {
                return database.getPost(postId);
        }
 
-       /**
-        * {@inheritDocs}
-        */
+       /** {@inheritDocs} */
        @Override
        public Collection<Post> getPosts(String soneId) {
                return database.getPosts(soneId);
        }
 
-       /**
-        * {@inheritDoc}
-        */
+       /** {@inheritDoc} */
        @Override
        public Collection<Post> getDirectedPosts(final String recipientId) {
                checkNotNull(recipientId, "recipient must not be null");
@@ -517,17 +500,13 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                return database.newPostReplyBuilder();
        }
 
-       /**
-        * {@inheritDoc}
-        */
+       /** {@inheritDoc} */
        @Override
        public Optional<PostReply> getPostReply(String replyId) {
                return database.getPostReply(replyId);
        }
 
-       /**
-        * {@inheritDoc}
-        */
+       /** {@inheritDoc} */
        @Override
        public List<PostReply> getReplies(final String postId) {
                return database.getReplies(postId);
@@ -537,7 +516,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * Returns all Sones that have liked the given post.
         *
         * @param post
-        *            The post to get the liking Sones for
+        *              The post to get the liking Sones for
         * @return The Sones that like the given post
         */
        public Set<Sone> getLikes(Post post) {
@@ -554,7 +533,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * Returns all Sones that have liked the given reply.
         *
         * @param reply
-        *            The reply to get the liking Sones for
+        *              The reply to get the liking Sones for
         * @return The Sones that like the given reply
         */
        public Set<Sone> getLikes(PostReply reply) {
@@ -571,7 +550,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * 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
         */
@@ -583,9 +562,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,7 +582,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                synchronized (bookmarkedPosts) {
                        for (String bookmarkedPostId : bookmarkedPosts) {
                                Optional<Post> post = getPost(bookmarkedPostId);
-                               if (!post.isPresent()) {
+                               if (post.isPresent()) {
                                        posts.add(post.get());
                                }
                        }
@@ -611,82 +590,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);
+       public Optional<Album> getAlbum(String albumId) {
+               return database.getAlbum(albumId);
        }
 
-       /**
-        * 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);
-       }
-
-       /**
-        * 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) {
@@ -699,12 +617,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) {
@@ -717,9 +634,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) {
@@ -733,7 +650,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) {
@@ -768,7 +685,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 +710,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) {
@@ -842,9 +759,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");
@@ -877,9 +794,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 +818,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 +836,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 +851,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 +863,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 +875,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 +887,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 +895,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());
@@ -1033,36 +950,38 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                                }
                        }
                        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 +1011,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,7 +1029,7 @@ 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()) {
@@ -1228,7 +1147,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++;
@@ -1244,19 +1163,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. */
@@ -1278,20 +1190,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().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).orNull());
                }
 
                /* load options. */
@@ -1300,7 +1210,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) {
@@ -1313,7 +1223,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) {
@@ -1337,9 +1249,9 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * Creates a new post.
         *
         * @param sone
-        *            The Sone that creates the post
+        *              The Sone that creates the post
         * @param text
-        *            The text of the post
+        *              The text of the post
         * @return The created post
         */
        public Post createPost(Sone sone, String text) {
@@ -1350,11 +1262,11 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * Creates a new post.
         *
         * @param sone
-        *            The Sone that creates the post
+        *              The Sone that creates the post
         * @param time
-        *            The time of the post
+        *              The time of the post
         * @param text
-        *            The text of the post
+        *              The text of the post
         * @return The created post
         */
        public Post createPost(Sone sone, long time, String text) {
@@ -1365,12 +1277,12 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * Creates a new post.
         *
         * @param sone
-        *            The Sone that creates the post
+        *              The Sone that creates the post
         * @param recipient
-        *            The recipient Sone, or {@code null} if this post does not have
-        *            a recipient
+        *              The recipient Sone, or {@code null} if this post does not have a
+        *              recipient
         * @param text
-        *            The text of the post
+        *              The text of the post
         * @return The created post
         */
        public Post createPost(Sone sone, Optional<Sone> recipient, String text) {
@@ -1381,14 +1293,14 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * Creates a new post.
         *
         * @param sone
-        *            The Sone that creates the post
+        *              The Sone that creates the post
         * @param recipient
-        *            The recipient Sone, or {@code null} if this post does not have
-        *            a recipient
+        *              The recipient Sone, or {@code null} if this post does not have a
+        *              recipient
         * @param time
-        *            The time of the post
+        *              The time of the post
         * @param text
-        *            The text of the post
+        *              The text of the post
         * @return The created post
         */
        public Post createPost(Sone sone, Optional<Sone> recipient, long time, String text) {
@@ -1425,7 +1337,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * Deletes the given post.
         *
         * @param post
-        *            The post to delete
+        *              The post to delete
         */
        public void deletePost(Post post) {
                if (!post.getSone().isLocal()) {
@@ -1443,7 +1355,7 @@ 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);
@@ -1458,7 +1370,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * Bookmarks the given post.
         *
         * @param post
-        *            The post to bookmark
+        *              The post to bookmark
         */
        public void bookmark(Post post) {
                bookmarkPost(post.getId());
@@ -1468,7 +1380,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * 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) {
@@ -1480,7 +1392,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());
@@ -1490,7 +1402,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) {
@@ -1502,11 +1414,11 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * Creates a new reply.
         *
         * @param sone
-        *            The Sone that creates the reply
+        *              The Sone that creates the reply
         * @param post
-        *            The post that this reply refers to
+        *              The post that this reply refers to
         * @param text
-        *            The text of the reply
+        *              The text of the reply
         * @return The created reply
         */
        public PostReply createReply(Sone sone, Post post, String text) {
@@ -1540,7 +1452,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * Deletes the given reply.
         *
         * @param reply
-        *            The reply to delete
+        *              The reply to delete
         */
        public void deleteReply(PostReply reply) {
                Sone sone = reply.getSone();
@@ -1559,7 +1471,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * (according to {@link Reply#isKnown()}).
         *
         * @param reply
-        *            The reply to mark as known
+        *              The reply to mark as known
         */
        public void markReplyKnown(PostReply reply) {
                boolean previouslyKnown = reply.isKnown();
@@ -1571,73 +1483,14 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
        }
 
        /**
-        * 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();
-       }
-
-       /**
         * 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) {
@@ -1646,31 +1499,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()).createdNow().sized(temporaryImage.getWidth(), temporaryImage.getHeight()).build();
                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(TemporaryImage)
         */
        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();
        }
 
@@ -1678,14 +1524,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);
                }
@@ -1696,7 +1541,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * Deletes the given temporary image.
         *
         * @param temporaryImage
-        *            The temporary image to delete
+        *              The temporary image to delete
         */
        public void deleteTemporaryImage(TemporaryImage temporaryImage) {
                checkNotNull(temporaryImage, "temporaryImage must not be null");
@@ -1707,23 +1552,22 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * 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();
@@ -1733,9 +1577,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
        // SERVICE METHODS
        //
 
-       /**
-        * Starts the core.
-        */
+       /** Starts the core. */
        @Override
        public void serviceStart() {
                loadConfiguration();
@@ -1746,9 +1588,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                database.start();
        }
 
-       /**
-        * {@inheritDoc}
-        */
+       /** {@inheritDoc} */
        @Override
        public void serviceRun() {
                long lastSaved = System.currentTimeMillis();
@@ -1765,9 +1605,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                }
        }
 
-       /**
-        * Stops the core.
-        */
+       /** Stops the core. */
        @Override
        public void serviceStop() {
                localElementTicker.shutdownNow();
@@ -1795,7 +1633,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()) {
@@ -1877,7 +1715,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 = FluentIterable.from(sone.getRootAlbum().getAlbums()).transformAndConcat(Album.FLATTENER).toList();
 
                        int albumCounter = 0;
                        for (Album album : albums) {
@@ -1885,7 +1723,7 @@ 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 + "/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);
@@ -1916,7 +1754,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();
 
@@ -1928,9 +1766,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                }
        }
 
-       /**
-        * Saves the current options.
-        */
+       /** Saves the current options. */
        private void saveConfiguration() {
                synchronized (configuration) {
                        if (storingConfiguration) {
@@ -2001,9 +1837,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>() {
@@ -2016,8 +1850,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)));
@@ -2093,11 +1927,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 {
@@ -2111,7 +1945,7 @@ 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) {
@@ -2126,7 +1960,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) {
@@ -2139,7 +1973,7 @@ 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) {
@@ -2153,7 +1987,7 @@ 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) {
@@ -2164,6 +1998,9 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                        @SuppressWarnings("synthetic-access")
                        public void run() {
                                Sone sone = getRemoteSone(identity.getId(), false);
+                               if (sone.isLocal()) {
+                                       return;
+                               }
                                sone.setIdentity(identity);
                                sone.setLatestEdition(Numbers.safeParseLong(identity.getProperty("Sone.LatestEdition"), sone.getLatestEdition()));
                                soneDownloader.addSone(sone);
@@ -2176,7 +2013,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) {
@@ -2219,12 +2056,12 @@ 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();
        }