🔀 Merge “release/v81” into “master”
[Sone.git] / src / main / java / net / pterodactylus / sone / core / Core.java
index f3927d9..18588a1 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - Core.java - Copyright © 2010–2013 David Roden
+ * Sone - Core.java - Copyright © 2010–2020 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -23,7 +23,10 @@ import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.primitives.Longs.tryParse;
 import static java.lang.String.format;
 import static java.util.logging.Level.WARNING;
+import static java.util.logging.Logger.getLogger;
+import static net.pterodactylus.sone.data.AlbumsKt.getAllImages;
 
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -35,28 +38,20 @@ import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.*;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import com.codahale.metrics.*;
 import net.pterodactylus.sone.core.ConfigurationSoneParser.InvalidAlbumFound;
 import net.pterodactylus.sone.core.ConfigurationSoneParser.InvalidImageFound;
 import net.pterodactylus.sone.core.ConfigurationSoneParser.InvalidParentAlbumFound;
 import net.pterodactylus.sone.core.ConfigurationSoneParser.InvalidPostFound;
 import net.pterodactylus.sone.core.ConfigurationSoneParser.InvalidPostReplyFound;
-import net.pterodactylus.sone.core.SoneChangeDetector.PostProcessor;
-import net.pterodactylus.sone.core.SoneChangeDetector.PostReplyProcessor;
-import net.pterodactylus.sone.core.event.ImageInsertFinishedEvent;
-import net.pterodactylus.sone.core.event.MarkPostKnownEvent;
-import net.pterodactylus.sone.core.event.MarkPostReplyKnownEvent;
-import net.pterodactylus.sone.core.event.MarkSoneKnownEvent;
-import net.pterodactylus.sone.core.event.NewPostFoundEvent;
-import net.pterodactylus.sone.core.event.NewPostReplyFoundEvent;
-import net.pterodactylus.sone.core.event.NewSoneFoundEvent;
-import net.pterodactylus.sone.core.event.PostRemovedEvent;
-import net.pterodactylus.sone.core.event.PostReplyRemovedEvent;
-import net.pterodactylus.sone.core.event.SoneLockedEvent;
-import net.pterodactylus.sone.core.event.SoneRemovedEvent;
-import net.pterodactylus.sone.core.event.SoneUnlockedEvent;
+import net.pterodactylus.sone.core.event.*;
 import net.pterodactylus.sone.data.Album;
 import net.pterodactylus.sone.data.Client;
 import net.pterodactylus.sone.data.Image;
@@ -66,8 +61,8 @@ import net.pterodactylus.sone.data.Profile;
 import net.pterodactylus.sone.data.Profile.Field;
 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.SoneOptions.LoadExternalContent;
 import net.pterodactylus.sone.data.TemporaryImage;
 import net.pterodactylus.sone.database.AlbumBuilder;
 import net.pterodactylus.sone.database.Database;
@@ -79,7 +74,6 @@ import net.pterodactylus.sone.database.PostReplyBuilder;
 import net.pterodactylus.sone.database.PostReplyProvider;
 import net.pterodactylus.sone.database.SoneBuilder;
 import net.pterodactylus.sone.database.SoneProvider;
-import net.pterodactylus.sone.fcp.FcpInterface;
 import net.pterodactylus.sone.freenet.wot.Identity;
 import net.pterodactylus.sone.freenet.wot.IdentityManager;
 import net.pterodactylus.sone.freenet.wot.OwnIdentity;
@@ -91,13 +85,11 @@ import net.pterodactylus.sone.freenet.wot.event.OwnIdentityRemovedEvent;
 import net.pterodactylus.sone.main.SonePlugin;
 import net.pterodactylus.util.config.Configuration;
 import net.pterodactylus.util.config.ConfigurationException;
-import net.pterodactylus.util.logging.Logging;
-import net.pterodactylus.util.number.Numbers;
 import net.pterodactylus.util.service.AbstractService;
 import net.pterodactylus.util.thread.NamedThreadFactory;
 
 import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Optional;
+import com.google.common.base.Stopwatch;
 import com.google.common.collect.FluentIterable;
 import com.google.common.collect.HashMultimap;
 import com.google.common.collect.Multimap;
@@ -106,21 +98,22 @@ import com.google.common.eventbus.EventBus;
 import com.google.common.eventbus.Subscribe;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
+import kotlin.jvm.functions.Function1;
 
 /**
  * The Sone core.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
 @Singleton
 public class Core extends AbstractService implements SoneProvider, PostProvider, PostReplyProvider {
 
        /** The logger. */
-       private static final Logger logger = Logging.getLogger(Core.class);
+       private static final Logger logger = getLogger(Core.class.getName());
 
        /** The start time. */
        private final long startupTime = System.currentTimeMillis();
 
+       private final AtomicBoolean debug = new AtomicBoolean(false);
+
        /** The preferences. */
        private final Preferences preferences;
 
@@ -154,39 +147,29 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
        /** The trust updater. */
        private final WebOfTrustUpdater webOfTrustUpdater;
 
-       /** The FCP interface. */
-       private volatile FcpInterface fcpInterface;
-
-       /** The times Sones were followed. */
-       private final Map<String, Long> soneFollowingTimes = new HashMap<String, Long>();
-
        /** Locked local Sones. */
        /* synchronize on itself. */
-       private final Set<Sone> lockedSones = new HashSet<Sone>();
+       private final Set<Sone> lockedSones = new HashSet<>();
 
        /** Sone inserters. */
        /* synchronize access on this on sones. */
-       private final Map<Sone, SoneInserter> soneInserters = new HashMap<Sone, SoneInserter>();
+       private final Map<Sone, SoneInserter> soneInserters = new HashMap<>();
 
        /** Sone rescuers. */
        /* synchronize access on this on sones. */
-       private final Map<Sone, SoneRescuer> soneRescuers = new HashMap<Sone, SoneRescuer>();
+       private final Map<Sone, SoneRescuer> soneRescuers = new HashMap<>();
 
        /** All known Sones. */
-       private final Set<String> knownSones = new HashSet<String>();
+       private final Set<String> knownSones = new HashSet<>();
 
        /** The post database. */
        private final Database database;
 
-       /** All bookmarked posts. */
-       /* synchronize access on itself. */
-       private final Set<String> bookmarkedPosts = new HashSet<String>();
-
        /** Trusted identities, sorted by own identities. */
        private final Multimap<OwnIdentity, Identity> trustedIdentities = Multimaps.synchronizedSetMultimap(HashMultimap.<OwnIdentity, Identity>create());
 
        /** All temporary images. */
-       private final Map<String, TemporaryImage> temporaryImages = new HashMap<String, TemporaryImage>();
+       private final Map<String, TemporaryImage> temporaryImages = new HashMap<>();
 
        /** Ticker for threads that mark own elements as known. */
        private final ScheduledExecutorService localElementTicker = Executors.newScheduledThreadPool(1);
@@ -194,6 +177,9 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
        /** The time the configuration was last touched. */
        private volatile long lastConfigurationUpdate;
 
+       private final MetricRegistry metricRegistry;
+       private final Histogram configurationSaveTimeHistogram;
+
        /**
         * Creates a new core.
         *
@@ -211,22 +197,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         *            The database
         */
        @Inject
-       public Core(Configuration configuration, FreenetInterface freenetInterface, IdentityManager identityManager, WebOfTrustUpdater webOfTrustUpdater, EventBus eventBus, Database database) {
-               super("Sone Core");
-               this.configuration = configuration;
-               this.freenetInterface = freenetInterface;
-               this.identityManager = identityManager;
-               this.soneDownloader = new SoneDownloaderImpl(this, freenetInterface);
-               this.imageInserter = new ImageInserter(freenetInterface, freenetInterface.new InsertTokenSupplier());
-               this.updateChecker = new UpdateChecker(eventBus, freenetInterface);
-               this.webOfTrustUpdater = webOfTrustUpdater;
-               this.eventBus = eventBus;
-               this.database = database;
-               preferences = new Preferences(eventBus);
-       }
-
-       @VisibleForTesting
-       protected Core(Configuration configuration, FreenetInterface freenetInterface, IdentityManager identityManager, SoneDownloader soneDownloader, ImageInserter imageInserter, UpdateChecker updateChecker, WebOfTrustUpdater webOfTrustUpdater, EventBus eventBus, Database database) {
+       public Core(Configuration configuration, FreenetInterface freenetInterface, IdentityManager identityManager, SoneDownloader soneDownloader, ImageInserter imageInserter, UpdateChecker updateChecker, WebOfTrustUpdater webOfTrustUpdater, EventBus eventBus, Database database, MetricRegistry metricRegistry) {
                super("Sone Core");
                this.configuration = configuration;
                this.freenetInterface = freenetInterface;
@@ -237,7 +208,9 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                this.webOfTrustUpdater = webOfTrustUpdater;
                this.eventBus = eventBus;
                this.database = database;
+               this.metricRegistry = metricRegistry;
                preferences = new Preferences(eventBus);
+               this.configurationSaveTimeHistogram = metricRegistry.histogram("configuration.save.duration", () -> new Histogram(new ExponentiallyDecayingReservoir(3000, 0)));
        }
 
        //
@@ -253,6 +226,16 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                return startupTime;
        }
 
+       @Nonnull
+       public boolean getDebug() {
+               return debug.get();
+       }
+
+       public void setDebug() {
+               debug.set(true);
+               eventBus.post(new DebugActivatedEvent());
+       }
+
        /**
         * Returns the options used by the core.
         *
@@ -281,16 +264,6 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
        }
 
        /**
-        * Sets the FCP interface to use.
-        *
-        * @param fcpInterface
-        *            The FCP interface to use
-        */
-       public void setFcpInterface(FcpInterface fcpInterface) {
-               this.fcpInterface = fcpInterface;
-       }
-
-       /**
         * Returns the Sone rescuer for the given local Sone.
         *
         * @param sone
@@ -331,11 +304,18 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
        /**
         * {@inheritDocs}
         */
+       @Nonnull
        @Override
        public Collection<Sone> getSones() {
                return database.getSones();
        }
 
+       @Nonnull
+       @Override
+       public Function1<String, Sone> getSoneLoader() {
+               return database.getSoneLoader();
+       }
+
        /**
         * Returns the Sone with the given ID, regardless whether it’s local or
         * remote.
@@ -346,7 +326,8 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         *         Sone
         */
        @Override
-       public Optional<Sone> getSone(String id) {
+       @Nullable
+       public Sone getSone(@Nonnull String id) {
                return database.getSone(id);
        }
 
@@ -366,9 +347,9 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * @return The Sone with the given ID, or {@code null}
         */
        public Sone getLocalSone(String id) {
-               Optional<Sone> sone = database.getSone(id);
-               if (sone.isPresent() && sone.get().isLocal()) {
-                       return sone.get();
+               Sone sone = database.getSone(id);
+               if ((sone != null) && sone.isLocal()) {
+                       return sone;
                }
                return null;
        }
@@ -390,7 +371,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * @return The Sone with the given ID
         */
        public Sone getRemoteSone(String id) {
-               return database.getSone(id).orNull();
+               return database.getSone(id);
        }
 
        /**
@@ -406,20 +387,6 @@ 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}
-        */
-       public long getSoneFollowingTime(Sone sone) {
-               synchronized (soneFollowingTimes) {
-                       return Optional.fromNullable(soneFollowingTimes.get(sone.getId())).or(Long.MAX_VALUE);
-               }
-       }
-
-       /**
         * Returns a post builder.
         *
         * @return A new post builder
@@ -428,11 +395,9 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                return database.newPostBuilder();
        }
 
-       /**
-        * {@inheritDoc}
-        */
+       @Nullable
        @Override
-       public Optional<Post> getPost(String postId) {
+       public Post getPost(@Nonnull String postId) {
                return database.getPost(postId);
        }
 
@@ -465,8 +430,9 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
        /**
         * {@inheritDoc}
         */
+       @Nullable
        @Override
-       public Optional<PostReply> getPostReply(String replyId) {
+       public PostReply getPostReply(String replyId) {
                return database.getPostReply(replyId);
        }
 
@@ -486,7 +452,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * @return The Sones that like the given post
         */
        public Set<Sone> getLikes(Post post) {
-               Set<Sone> sones = new HashSet<Sone>();
+               Set<Sone> sones = new HashSet<>();
                for (Sone sone : getSones()) {
                        if (sone.getLikedPostIds().contains(post.getId())) {
                                sones.add(sone);
@@ -503,7 +469,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * @return The Sones that like the given reply
         */
        public Set<Sone> getLikes(PostReply reply) {
-               Set<Sone> sones = new HashSet<Sone>();
+               Set<Sone> sones = new HashSet<>();
                for (Sone sone : getSones()) {
                        if (sone.getLikedReplyIds().contains(reply.getId())) {
                                sones.add(sone);
@@ -521,21 +487,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         *         otherwise
         */
        public boolean isBookmarked(Post post) {
-               return isPostBookmarked(post.getId());
-       }
-
-       /**
-        * Returns whether the post with the given ID is bookmarked.
-        *
-        * @param id
-        *            The ID of the post to check
-        * @return {@code true} if the post with the given ID is bookmarked,
-        *         {@code false} otherwise
-        */
-       public boolean isPostBookmarked(String id) {
-               synchronized (bookmarkedPosts) {
-                       return bookmarkedPosts.contains(id);
-               }
+               return database.isPostBookmarked(post);
        }
 
        /**
@@ -544,16 +496,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * @return All bookmarked posts
         */
        public Set<Post> getBookmarkedPosts() {
-               Set<Post> posts = new HashSet<Post>();
-               synchronized (bookmarkedPosts) {
-                       for (String bookmarkedPostId : bookmarkedPosts) {
-                               Optional<Post> post = getPost(bookmarkedPostId);
-                               if (post.isPresent()) {
-                                       posts.add(post.get());
-                               }
-                       }
-               }
-               return posts;
+               return database.getBookmarkedPosts();
        }
 
        public AlbumBuilder albumBuilder() {
@@ -569,8 +512,9 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * @return The album with the given ID, or {@code null} if no album with the
         *         given ID exists
         */
-       public Album getAlbum(String albumId) {
-               return database.getAlbum(albumId).orNull();
+       @Nullable
+       public Album getAlbum(@Nonnull String albumId) {
+               return database.getAlbum(albumId);
        }
 
        public ImageBuilder imageBuilder() {
@@ -584,6 +528,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         *            The ID of the image
         * @return The image with the given ID
         */
+       @Nullable
        public Image getImage(String imageId) {
                return getImage(imageId, true);
        }
@@ -600,10 +545,11 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * @return The image with the given ID, or {@code null} if none exists and
         *         none was created
         */
+       @Nullable
        public Image getImage(String imageId, boolean create) {
-               Optional<Image> image = database.getImage(imageId);
-               if (image.isPresent()) {
-                       return image.get();
+               Image image = database.getImage(imageId);
+               if (image != null) {
+                       return image;
                }
                if (!create) {
                        return null;
@@ -676,17 +622,24 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                }
                logger.info(String.format("Adding Sone from OwnIdentity: %s", ownIdentity));
                Sone sone = database.newSoneBuilder().local().from(ownIdentity).build();
-               sone.setLatestEdition(Numbers.safeParseLong(ownIdentity.getProperty("Sone.LatestEdition"), 0L));
-               sone.setClient(new Client("Sone", SonePlugin.VERSION.toString()));
+               String property = fromNullable(ownIdentity.getProperty("Sone.LatestEdition")).or("0");
+               sone.setLatestEdition(fromNullable(tryParse(property)).or(0L));
+               sone.setClient(new Client("Sone", SonePlugin.getPluginVersion()));
                sone.setKnown(true);
-               /* TODO - load posts ’n stuff */
-               SoneInserter soneInserter = new SoneInserter(this, eventBus, freenetInterface, ownIdentity.getId());
+               SoneInserter soneInserter = new SoneInserter(this, eventBus, freenetInterface, metricRegistry, ownIdentity.getId());
+               soneInserter.insertionDelayChanged(new InsertionDelayChangedEvent(preferences.getInsertionDelay()));
                eventBus.register(soneInserter);
                synchronized (soneInserters) {
                        soneInserters.put(sone, soneInserter);
                }
                loadSone(sone);
+               database.storeSone(sone);
                sone.setStatus(SoneStatus.idle);
+               if (sone.getPosts().isEmpty() && sone.getReplies().isEmpty() && getAllImages(sone.getRootAlbum()).isEmpty()) {
+                       // dirty hack
+                       lockSone(sone);
+                       eventBus.post(new SoneLockedOnStartup(sone));
+               }
                soneInserter.start();
                return sone;
        }
@@ -722,14 +675,14 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                        logger.log(Level.WARNING, "Given Identity is null!");
                        return null;
                }
-               final Long latestEdition = tryParse(fromNullable(
-                               identity.getProperty("Sone.LatestEdition")).or("0"));
-               Optional<Sone> existingSone = getSone(identity.getId());
-               if (existingSone.isPresent() && existingSone.get().isLocal()) {
-                       return existingSone.get();
+               String property = fromNullable(identity.getProperty("Sone.LatestEdition")).or("0");
+               long latestEdition = fromNullable(tryParse(property)).or(0L);
+               Sone existingSone = getSone(identity.getId());
+               if ((existingSone != null )&& existingSone.isLocal()) {
+                       return existingSone;
                }
-               boolean newSone = !existingSone.isPresent();
-               Sone sone = !newSone ? existingSone.get() : database.newSoneBuilder().from(identity).build();
+               boolean newSone = existingSone == null;
+               Sone sone = !newSone ? existingSone : database.newSoneBuilder().from(identity).build();
                sone.setLatestEdition(latestEdition);
                if (newSone) {
                        synchronized (knownSones) {
@@ -747,7 +700,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                }
                database.storeSone(sone);
                soneDownloader.addSone(sone);
-               soneDownloaders.execute(soneDownloader.fetchSoneWithUriAction(sone));
+               soneDownloaders.execute(soneDownloader.fetchSoneAsUskAction(sone));
                return sone;
        }
 
@@ -762,25 +715,21 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
        public void followSone(Sone sone, String soneId) {
                checkNotNull(sone, "sone must not be null");
                checkNotNull(soneId, "soneId must not be null");
-               sone.addFriend(soneId);
-               synchronized (soneFollowingTimes) {
-                       if (!soneFollowingTimes.containsKey(soneId)) {
-                               long now = System.currentTimeMillis();
-                               soneFollowingTimes.put(soneId, now);
-                               Optional<Sone> followedSone = getSone(soneId);
-                               if (!followedSone.isPresent()) {
-                                       return;
-                               }
-                               for (Post post : followedSone.get().getPosts()) {
-                                       if (post.getTime() < now) {
-                                               markPostKnown(post);
-                                       }
-                               }
-                               for (PostReply reply : followedSone.get().getReplies()) {
-                                       if (reply.getTime() < now) {
-                                               markReplyKnown(reply);
-                                       }
-                               }
+               database.addFriend(sone, soneId);
+               @SuppressWarnings("ConstantConditions") // we just followed, this can’t be null.
+               long now = database.getFollowingTime(soneId);
+               Sone followedSone = getSone(soneId);
+               if (followedSone == null) {
+                       return;
+               }
+               for (Post post : followedSone.getPosts()) {
+                       if (post.getTime() < now) {
+                               markPostKnown(post);
+                       }
+               }
+               for (PostReply reply : followedSone.getReplies()) {
+                       if (reply.getTime() < now) {
+                               markReplyKnown(reply);
                        }
                }
                touchConfiguration();
@@ -797,89 +746,11 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
        public void unfollowSone(Sone sone, String soneId) {
                checkNotNull(sone, "sone must not be null");
                checkNotNull(soneId, "soneId must not be null");
-               sone.removeFriend(soneId);
-               boolean unfollowedSoneStillFollowed = false;
-               for (Sone localSone : getLocalSones()) {
-                       unfollowedSoneStillFollowed |= localSone.hasFriend(soneId);
-               }
-               if (!unfollowedSoneStillFollowed) {
-                       synchronized (soneFollowingTimes) {
-                               soneFollowingTimes.remove(soneId);
-                       }
-               }
+               database.removeFriend(sone, soneId);
                touchConfiguration();
        }
 
        /**
-        * Sets the trust value of the given origin Sone for the target Sone.
-        *
-        * @param origin
-        *            The origin Sone
-        * @param target
-        *            The target Sone
-        * @param trustValue
-        *            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");
-               checkArgument(origin.getIdentity() instanceof OwnIdentity, "origin must be a local Sone");
-               checkNotNull(target, "target must not be null");
-               checkArgument((trustValue >= -100) && (trustValue <= 100), "trustValue must be within [-100, 100]");
-               webOfTrustUpdater.setTrust((OwnIdentity) origin.getIdentity(), target.getIdentity(), trustValue, preferences.getTrustComment());
-       }
-
-       /**
-        * Removes any trust assignment for the given target Sone.
-        *
-        * @param origin
-        *            The trust origin
-        * @param target
-        *            The trust target
-        */
-       public void removeTrust(Sone origin, Sone target) {
-               checkNotNull(origin, "origin must not be null");
-               checkNotNull(target, "target must not be null");
-               checkArgument(origin.getIdentity() instanceof OwnIdentity, "origin must be a local Sone");
-               webOfTrustUpdater.setTrust((OwnIdentity) origin.getIdentity(), target.getIdentity(), null, null);
-       }
-
-       /**
-        * Assigns the configured positive trust value for the given target.
-        *
-        * @param origin
-        *            The trust origin
-        * @param target
-        *            The trust target
-        */
-       public void trustSone(Sone origin, Sone target) {
-               setTrust(origin, target, preferences.getPositiveTrust());
-       }
-
-       /**
-        * Assigns the configured negative trust value for the given target.
-        *
-        * @param origin
-        *            The trust origin
-        * @param target
-        *            The trust target
-        */
-       public void distrustSone(Sone origin, Sone target) {
-               setTrust(origin, target, preferences.getNegativeTrust());
-       }
-
-       /**
-        * Removes the trust assignment for the given target.
-        *
-        * @param origin
-        *            The trust origin
-        * @param target
-        *            The trust target
-        */
-       public void untrustSone(Sone origin, Sone target) {
-               removeTrust(origin, target);
-       }
-
-       /**
         * Updates the stored Sone with the given Sone.
         *
         * @param sone
@@ -901,50 +772,20 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         *            of the age of the given Sone
         */
        public void updateSone(final Sone sone, boolean soneRescueMode) {
-               Optional<Sone> storedSone = getSone(sone.getId());
-               if (storedSone.isPresent()) {
-                       if (!soneRescueMode && !(sone.getTime() > storedSone.get().getTime())) {
+               Sone storedSone = getSone(sone.getId());
+               if (storedSone != null) {
+                       if (!soneRescueMode && !(sone.getTime() > storedSone.getTime())) {
                                logger.log(Level.FINE, String.format("Downloaded Sone %s is not newer than stored Sone %s.", sone, storedSone));
                                return;
                        }
-                       /* find removed posts. */
-                       SoneChangeDetector soneChangeDetector = new SoneChangeDetector(storedSone.get());
-                       soneChangeDetector.onNewPosts(new PostProcessor() {
-                               @Override
-                               public void processPost(Post post) {
-                                       if (post.getTime() < getSoneFollowingTime(sone)) {
-                                               post.setKnown(true);
-                                       } else if (!post.isKnown()) {
-                                               eventBus.post(new NewPostFoundEvent(post));
-                                       }
-                               }
-                       });
-                       soneChangeDetector.onRemovedPosts(new PostProcessor() {
-                               @Override
-                               public void processPost(Post post) {
-                                       eventBus.post(new PostRemovedEvent(post));
-                               }
-                       });
-                       soneChangeDetector.onNewPostReplies(new PostReplyProcessor() {
-                               @Override
-                               public void processPostReply(PostReply postReply) {
-                                       if (postReply.getTime() < getSoneFollowingTime(sone)) {
-                                               postReply.setKnown(true);
-                                       } else if (!postReply.isKnown()) {
-                                               eventBus.post(new NewPostReplyFoundEvent(postReply));
-                                       }
-                               }
-                       });
-                       soneChangeDetector.onRemovedPostReplies(new PostReplyProcessor() {
-                               @Override
-                               public void processPostReply(PostReply postReply) {
-                                       eventBus.post(new PostReplyRemovedEvent(postReply));
-                               }
-                       });
-                       soneChangeDetector.detectChanges(sone);
+                       List<Object> events =
+                                       collectEventsForChangesInSone(storedSone, sone);
                        database.storeSone(sone);
-                       sone.setOptions(storedSone.get().getOptions());
-                       sone.setKnown(storedSone.get().isKnown());
+                       for (Object event : events) {
+                               eventBus.post(event);
+                       }
+                       sone.setOptions(storedSone.getOptions());
+                       sone.setKnown(storedSone.isKnown());
                        sone.setStatus((sone.getTime() == 0) ? SoneStatus.unknown : SoneStatus.idle);
                        if (sone.isLocal()) {
                                touchConfiguration();
@@ -952,6 +793,36 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                }
        }
 
+       private List<Object> collectEventsForChangesInSone(Sone oldSone, Sone newSone) {
+               List<Object> events = new ArrayList<>();
+               SoneComparison soneComparison = new SoneComparison(oldSone, newSone);
+               for (Post newPost : soneComparison.getNewPosts()) {
+                       if (newPost.getSone().equals(newSone)) {
+                               newPost.setKnown(true);
+                       } else if (newPost.getTime() < database.getFollowingTime(newSone.getId())) {
+                               newPost.setKnown(true);
+                       } else if (!newPost.isKnown()) {
+                               events.add(new NewPostFoundEvent(newPost));
+                       }
+               }
+               for (Post post : soneComparison.getRemovedPosts()) {
+                       events.add(new PostRemovedEvent(post));
+               }
+               for (PostReply postReply : soneComparison.getNewPostReplies()) {
+                       if (postReply.getSone().equals(newSone)) {
+                               postReply.setKnown(true);
+                       } else if (postReply.getTime() < database.getFollowingTime(newSone.getId())) {
+                               postReply.setKnown(true);
+                       } else if (!postReply.isKnown()) {
+                               events.add(new NewPostReplyFoundEvent(postReply));
+                       }
+               }
+               for (PostReply postReply : soneComparison.getRemovedPostReplies()) {
+                       events.add(new PostReplyRemovedEvent(postReply));
+               }
+               return events;
+       }
+
        /**
         * Deletes the given Sone. This will remove the Sone from the
         * {@link #getLocalSones() local Sones}, stop its {@link SoneInserter} and
@@ -969,10 +840,9 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                        logger.log(Level.WARNING, String.format("Tried to delete non-local Sone: %s", sone));
                        return;
                }
-               // FIXME – implement in database
-//             sones.remove(sone.getId());
                SoneInserter soneInserter = soneInserters.remove(sone);
                soneInserter.stop();
+               database.removeSone(sone);
                webOfTrustUpdater.removeContext((OwnIdentity) sone.getIdentity(), "Sone");
                webOfTrustUpdater.removeProperty((OwnIdentity) sone.getIdentity(), "Sone.LatestEdition");
                try {
@@ -1053,9 +923,6 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                Set<String> likedReplyIds =
                                configurationSoneParser.parseLikedPostReplyIds();
 
-               /* load friends. */
-               Set<String> friends = configurationSoneParser.parseFriends();
-
                /* load albums. */
                List<Album> topLevelAlbums;
                try {
@@ -1092,12 +959,13 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                }
 
                /* load options. */
-               sone.getOptions().setAutoFollow(configuration.getBooleanValue(sonePrefix + "/Options/AutoFollow").getValue(null));
-               sone.getOptions().setSoneInsertNotificationEnabled(configuration.getBooleanValue(sonePrefix + "/Options/EnableSoneInsertNotifications").getValue(null));
-               sone.getOptions().setShowNewSoneNotifications(configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewSones").getValue(null));
-               sone.getOptions().setShowNewPostNotifications(configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewPosts").getValue(null));
-               sone.getOptions().setShowNewReplyNotifications(configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewReplies").getValue(null));
-               sone.getOptions().setShowCustomAvatars(ShowCustomAvatars.valueOf(configuration.getStringValue(sonePrefix + "/Options/ShowCustomAvatars").getValue(ShowCustomAvatars.NEVER.name())));
+               sone.getOptions().setAutoFollow(configuration.getBooleanValue(sonePrefix + "/Options/AutoFollow").getValue(false));
+               sone.getOptions().setSoneInsertNotificationEnabled(configuration.getBooleanValue(sonePrefix + "/Options/EnableSoneInsertNotifications").getValue(false));
+               sone.getOptions().setShowNewSoneNotifications(configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewSones").getValue(true));
+               sone.getOptions().setShowNewPostNotifications(configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewPosts").getValue(true));
+               sone.getOptions().setShowNewReplyNotifications(configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewReplies").getValue(true));
+               sone.getOptions().setShowCustomAvatars(LoadExternalContent.valueOf(configuration.getStringValue(sonePrefix + "/Options/ShowCustomAvatars").getValue(LoadExternalContent.NEVER.name())));
+               sone.getOptions().setLoadLinkedImages(LoadExternalContent.valueOf(configuration.getStringValue(sonePrefix + "/Options/LoadLinkedImages").getValue(LoadExternalContent.NEVER.name())));
 
                /* if we’re still here, Sone was loaded successfully. */
                synchronized (sone) {
@@ -1107,25 +975,16 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                        sone.setReplies(replies);
                        sone.setLikePostIds(likedPostIds);
                        sone.setLikeReplyIds(likedReplyIds);
-                       for (String friendId : friends) {
-                               followSone(sone, friendId);
-                       }
                        for (Album album : sone.getRootAlbum().getAlbums()) {
                                sone.getRootAlbum().removeAlbum(album);
                        }
                        for (Album album : topLevelAlbums) {
                                sone.getRootAlbum().addAlbum(album);
                        }
-                       database.storeSone(sone);
                        synchronized (soneInserters) {
                                soneInserters.get(sone).setLastInsertFingerprint(lastInsertFingerprint);
                        }
                }
-               synchronized (knownSones) {
-                       for (String friend : friends) {
-                               knownSones.add(friend);
-                       }
-               }
                for (Post post : posts) {
                        post.setKnown(true);
                }
@@ -1148,25 +1007,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         *            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) {
+       public Post createPost(Sone sone, @Nullable Sone recipient, String text) {
                checkNotNull(text, "text must not be null");
                checkArgument(text.trim().length() > 0, "text must not be empty");
                if (!sone.isLocal()) {
@@ -1174,9 +1015,9 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                        return null;
                }
                PostBuilder postBuilder = database.newPostBuilder();
-               postBuilder.from(sone.getId()).randomId().withTime(time).withText(text.trim());
-               if (recipient.isPresent()) {
-                       postBuilder.to(recipient.get().getId());
+               postBuilder.from(sone.getId()).randomId().currentTime().withText(text.trim());
+               if (recipient != null) {
+                       postBuilder.to(recipient.getId());
                }
                final Post post = postBuilder.build();
                database.storePost(post);
@@ -1220,16 +1061,8 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                }
        }
 
-       /**
-        * Bookmarks the post with the given ID.
-        *
-        * @param id
-        *            The ID of the post to bookmark
-        */
-       public void bookmarkPost(String id) {
-               synchronized (bookmarkedPosts) {
-                       bookmarkedPosts.add(id);
-               }
+       public void bookmarkPost(Post post) {
+               database.bookmarkPost(post);
        }
 
        /**
@@ -1238,20 +1071,8 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         * @param post
         *            The post to unbookmark
         */
-       public void unbookmark(Post post) {
-               unbookmarkPost(post.getId());
-       }
-
-       /**
-        * Removes the post with the given ID from the bookmarks.
-        *
-        * @param id
-        *            The ID of the post to unbookmark
-        */
-       public void unbookmarkPost(String id) {
-               synchronized (bookmarkedPosts) {
-                       bookmarkedPosts.remove(id);
-               }
+       public void unbookmarkPost(Post post) {
+               database.unbookmarkPost(post);
        }
 
        /**
@@ -1451,7 +1272,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                identityManager.start();
                webOfTrustUpdater.init();
                webOfTrustUpdater.start();
-               database.start();
+               database.startAsync();
        }
 
        /**
@@ -1482,11 +1303,17 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                synchronized (soneInserters) {
                        for (Entry<Sone, SoneInserter> soneInserter : soneInserters.entrySet()) {
                                soneInserter.getValue().stop();
-                               saveSone(soneInserter.getKey());
+                               Sone latestSone = getLocalSone(soneInserter.getKey().getId());
+                               saveSone(latestSone);
+                       }
+               }
+               synchronized (soneRescuers) {
+                       for (SoneRescuer soneRescuer : soneRescuers.values()) {
+                               soneRescuer.stop();
                        }
                }
                saveConfiguration();
-               database.stop();
+               database.stopAsync();
                webOfTrustUpdater.stop();
                updateChecker.stop();
                soneDownloader.stop();
@@ -1577,13 +1404,6 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                        }
                        configuration.getStringValue(sonePrefix + "/Likes/Reply/" + replyLikeCounter + "/ID").setValue(null);
 
-                       /* save friends. */
-                       int friendCounter = 0;
-                       for (String friendId : sone.getFriends()) {
-                               configuration.getStringValue(sonePrefix + "/Friends/" + friendCounter++ + "/ID").setValue(friendId);
-                       }
-                       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.getRootAlbum().getAlbums()).transformAndConcat(Album.FLATTENER).toList();
 
@@ -1594,7 +1414,6 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                                configuration.getStringValue(albumPrefix + "/Title").setValue(album.getTitle());
                                configuration.getStringValue(albumPrefix + "/Description").setValue(album.getDescription());
                                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);
 
@@ -1625,8 +1444,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                        configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewPosts").setValue(sone.getOptions().isShowNewPostNotifications());
                        configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewReplies").setValue(sone.getOptions().isShowNewReplyNotifications());
                        configuration.getStringValue(sonePrefix + "/Options/ShowCustomAvatars").setValue(sone.getOptions().getShowCustomAvatars().name());
-
-                       configuration.save();
+                       configuration.getStringValue(sonePrefix + "/Options/LoadLinkedImages").setValue(sone.getOptions().getLoadLinkedImages().name());
 
                        webOfTrustUpdater.setProperty((OwnIdentity) sone.getIdentity(), "Sone.LatestEdition", String.valueOf(sone.getLatestEdition()));
 
@@ -1661,31 +1479,13 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                                configuration.getStringValue("KnownSone/" + soneCounter + "/ID").setValue(null);
                        }
 
-                       /* save Sone following times. */
-                       soneCounter = 0;
-                       synchronized (soneFollowingTimes) {
-                               for (Entry<String, Long> soneFollowingTime : soneFollowingTimes.entrySet()) {
-                                       configuration.getStringValue("SoneFollowingTimes/" + soneCounter + "/Sone").setValue(soneFollowingTime.getKey());
-                                       configuration.getLongValue("SoneFollowingTimes/" + soneCounter + "/Time").setValue(soneFollowingTime.getValue());
-                                       ++soneCounter;
-                               }
-                               configuration.getStringValue("SoneFollowingTimes/" + soneCounter + "/Sone").setValue(null);
-                       }
-
                        /* save known posts. */
                        database.save();
 
-                       /* save bookmarked posts. */
-                       int bookmarkedPostCounter = 0;
-                       synchronized (bookmarkedPosts) {
-                               for (String bookmarkedPostId : bookmarkedPosts) {
-                                       configuration.getStringValue("Bookmarks/Post/" + bookmarkedPostCounter++ + "/ID").setValue(bookmarkedPostId);
-                               }
-                       }
-                       configuration.getStringValue("Bookmarks/Post/" + bookmarkedPostCounter++ + "/ID").setValue(null);
-
                        /* now save it. */
+                       Stopwatch stopwatch = Stopwatch.createStarted();
                        configuration.save();
+                       configurationSaveTimeHistogram.update(stopwatch.elapsed(TimeUnit.MICROSECONDS));
 
                } catch (ConfigurationException ce1) {
                        logger.log(Level.SEVERE, "Could not store configuration!", ce1);
@@ -1715,33 +1515,6 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                                knownSones.add(knownSoneId);
                        }
                }
-
-               /* load Sone following times. */
-               soneCounter = 0;
-               while (true) {
-                       String soneId = configuration.getStringValue("SoneFollowingTimes/" + soneCounter + "/Sone").getValue(null);
-                       if (soneId == null) {
-                               break;
-                       }
-                       long time = configuration.getLongValue("SoneFollowingTimes/" + soneCounter + "/Time").getValue(Long.MAX_VALUE);
-                       synchronized (soneFollowingTimes) {
-                               soneFollowingTimes.put(soneId, time);
-                       }
-                       ++soneCounter;
-               }
-
-               /* load bookmarked posts. */
-               int bookmarkedPostCounter = 0;
-               while (true) {
-                       String bookmarkedPostId = configuration.getStringValue("Bookmarks/Post/" + bookmarkedPostCounter++ + "/ID").getValue(null);
-                       if (bookmarkedPostId == null) {
-                               break;
-                       }
-                       synchronized (bookmarkedPosts) {
-                               bookmarkedPosts.add(bookmarkedPostId);
-                       }
-               }
-
        }
 
        /**
@@ -1752,7 +1525,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         */
        @Subscribe
        public void ownIdentityAdded(OwnIdentityAddedEvent ownIdentityAddedEvent) {
-               OwnIdentity ownIdentity = ownIdentityAddedEvent.ownIdentity();
+               OwnIdentity ownIdentity = ownIdentityAddedEvent.getOwnIdentity();
                logger.log(Level.FINEST, String.format("Adding OwnIdentity: %s", ownIdentity));
                if (ownIdentity.hasContext("Sone")) {
                        addLocalSone(ownIdentity);
@@ -1767,7 +1540,7 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         */
        @Subscribe
        public void ownIdentityRemoved(OwnIdentityRemovedEvent ownIdentityRemovedEvent) {
-               OwnIdentity ownIdentity = ownIdentityRemovedEvent.ownIdentity();
+               OwnIdentity ownIdentity = ownIdentityRemovedEvent.getOwnIdentity();
                logger.log(Level.FINEST, String.format("Removing OwnIdentity: %s", ownIdentity));
                trustedIdentities.removeAll(ownIdentity);
        }
@@ -1780,9 +1553,9 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         */
        @Subscribe
        public void identityAdded(IdentityAddedEvent identityAddedEvent) {
-               Identity identity = identityAddedEvent.identity();
+               Identity identity = identityAddedEvent.getIdentity();
                logger.log(Level.FINEST, String.format("Adding Identity: %s", identity));
-               trustedIdentities.put(identityAddedEvent.ownIdentity(), identity);
+               trustedIdentities.put(identityAddedEvent.getOwnIdentity(), identity);
                addRemoteSone(identity);
        }
 
@@ -1794,14 +1567,20 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         */
        @Subscribe
        public void identityUpdated(IdentityUpdatedEvent identityUpdatedEvent) {
-               Identity identity = identityUpdatedEvent.identity();
+               Identity identity = identityUpdatedEvent.getIdentity();
                final Sone sone = getRemoteSone(identity.getId());
                if (sone.isLocal()) {
                        return;
                }
-               sone.setLatestEdition(Numbers.safeParseLong(identity.getProperty("Sone.LatestEdition"), sone.getLatestEdition()));
+               String newLatestEdition = identity.getProperty("Sone.LatestEdition");
+               if (newLatestEdition != null) {
+                       Long parsedNewLatestEdition = tryParse(newLatestEdition);
+                       if (parsedNewLatestEdition != null) {
+                               sone.setLatestEdition(parsedNewLatestEdition);
+                       }
+               }
                soneDownloader.addSone(sone);
-               soneDownloaders.execute(soneDownloader.fetchSoneAction(sone));
+               soneDownloaders.execute(soneDownloader.fetchSoneAsSskAction(sone));
        }
 
        /**
@@ -1812,8 +1591,8 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         */
        @Subscribe
        public void identityRemoved(IdentityRemovedEvent identityRemovedEvent) {
-               OwnIdentity ownIdentity = identityRemovedEvent.ownIdentity();
-               Identity identity = identityRemovedEvent.identity();
+               OwnIdentity ownIdentity = identityRemovedEvent.getOwnIdentity();
+               Identity identity = identityRemovedEvent.getIdentity();
                trustedIdentities.remove(ownIdentity, identity);
                for (Entry<OwnIdentity, Collection<Identity>> trustedIdentity : trustedIdentities.asMap().entrySet()) {
                        if (trustedIdentity.getKey().equals(ownIdentity)) {
@@ -1823,22 +1602,19 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                                return;
                        }
                }
-               Optional<Sone> sone = getSone(identity.getId());
-               if (!sone.isPresent()) {
+               Sone sone = getSone(identity.getId());
+               if (sone == null) {
                        /* TODO - we don’t have the Sone anymore. should this happen? */
                        return;
                }
-               database.removePosts(sone.get());
-               for (Post post : sone.get().getPosts()) {
-                       eventBus.post(new PostRemovedEvent(post));
+               for (PostReply postReply : sone.getReplies()) {
+                       eventBus.post(new PostReplyRemovedEvent(postReply));
                }
-               database.removePostReplies(sone.get());
-               for (PostReply reply : sone.get().getReplies()) {
-                       eventBus.post(new PostReplyRemovedEvent(reply));
+               for (Post post : sone.getPosts()) {
+                       eventBus.post(new PostRemovedEvent(post));
                }
-//             TODO – implement in database
-//             sones.remove(identity.getId());
-               eventBus.post(new SoneRemovedEvent(sone.get()));
+               eventBus.post(new SoneRemovedEvent(sone));
+               database.removeSone(sone);
        }
 
        /**
@@ -1849,9 +1625,9 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
         */
        @Subscribe
        public void imageInsertFinished(ImageInsertFinishedEvent imageInsertFinishedEvent) {
-               logger.log(Level.WARNING, String.format("Image insert finished for %s: %s", imageInsertFinishedEvent.image(), imageInsertFinishedEvent.resultingUri()));
-               imageInsertFinishedEvent.image().modify().setKey(imageInsertFinishedEvent.resultingUri().toString()).update();
-               deleteTemporaryImage(imageInsertFinishedEvent.image().getId());
+               logger.log(Level.WARNING, String.format("Image insert finished for %s: %s", imageInsertFinishedEvent.getImage(), imageInsertFinishedEvent.getResultingUri()));
+               imageInsertFinishedEvent.getImage().modify().setKey(imageInsertFinishedEvent.getResultingUri().toString()).update();
+               deleteTemporaryImage(imageInsertFinishedEvent.getImage().getId());
                touchConfiguration();
        }