Merge branch 'next' into new-database-38
authorDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Thu, 13 Sep 2012 12:34:58 +0000 (14:34 +0200)
committerDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Thu, 13 Sep 2012 14:19:41 +0000 (16:19 +0200)
Conflicts:
src/main/java/net/pterodactylus/sone/core/Core.java
src/main/java/net/pterodactylus/sone/data/Sone.java
src/main/java/net/pterodactylus/sone/main/SonePlugin.java
src/main/java/net/pterodactylus/sone/text/SoneTextParser.java

1  2 
src/main/java/net/pterodactylus/sone/core/Core.java
src/main/java/net/pterodactylus/sone/database/memory/MemorySone.java
src/main/java/net/pterodactylus/sone/main/SonePlugin.java
src/main/java/net/pterodactylus/sone/template/ProfileAccessor.java
src/main/java/net/pterodactylus/sone/template/SoneAccessor.java
src/main/java/net/pterodactylus/sone/text/SoneTextParser.java
src/main/java/net/pterodactylus/sone/web/SearchPage.java
src/main/java/net/pterodactylus/sone/web/WebInterface.java
src/main/java/net/pterodactylus/sone/web/ajax/EditImageAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/GetLikesAjaxPage.java

@@@ -19,7 -19,6 +19,7 @@@ package net.pterodactylus.sone.core
  
  import java.net.MalformedURLException;
  import java.util.ArrayList;
 +import java.util.Collection;
  import java.util.Collections;
  import java.util.HashMap;
  import java.util.HashSet;
@@@ -47,15 -46,13 +47,14 @@@ import net.pterodactylus.sone.data.Sone
  import net.pterodactylus.sone.data.Sone.ShowCustomAvatars;
  import net.pterodactylus.sone.data.Sone.SoneStatus;
  import net.pterodactylus.sone.data.TemporaryImage;
+ import net.pterodactylus.sone.data.impl.PostImpl;
 +import net.pterodactylus.sone.database.Database;
  import net.pterodactylus.sone.fcp.FcpInterface;
  import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired;
  import net.pterodactylus.sone.freenet.wot.Identity;
  import net.pterodactylus.sone.freenet.wot.IdentityListener;
  import net.pterodactylus.sone.freenet.wot.IdentityManager;
  import net.pterodactylus.sone.freenet.wot.OwnIdentity;
- import net.pterodactylus.sone.freenet.wot.Trust;
- import net.pterodactylus.sone.freenet.wot.WebOfTrustException;
  import net.pterodactylus.sone.main.SonePlugin;
  import net.pterodactylus.util.config.Configuration;
  import net.pterodactylus.util.config.ConfigurationException;
@@@ -95,9 -92,6 +94,9 @@@ public class Core extends AbstractServi
        /** Whether we’re currently saving the configuration. */
        private boolean storingConfiguration = false;
  
 +      /** The database. */
 +      private Database database;
 +
        /** The identity manager. */
        private final IdentityManager identityManager;
  
        /** The update checker. */
        private final UpdateChecker updateChecker;
  
+       /** The trust updater. */
+       private final WebOfTrustUpdater webOfTrustUpdater;
        /** The FCP interface. */
        private volatile FcpInterface fcpInterface;
  
        /* synchronize access on this on localSones. */
        private final Map<Sone, SoneRescuer> soneRescuers = new HashMap<Sone, SoneRescuer>();
  
 -      /** All local Sones. */
 -      /* synchronize access on this on itself. */
 -      private final Map<String, Sone> localSones = new HashMap<String, Sone>();
 -
 -      /** All remote Sones. */
 -      /* synchronize access on this on itself. */
 -      private final Map<String, Sone> remoteSones = new HashMap<String, Sone>();
 -
        /** All known Sones. */
        private final Set<String> knownSones = new HashSet<String>();
  
         *
         * @param configuration
         *            The configuration of the core
 +       * @param database
 +       *            The database to use
         * @param freenetInterface
         *            The freenet interface
         * @param identityManager
         *            The identity manager
+        * @param webOfTrustUpdater
+        *            The WebOfTrust updater
         */
-       public Core(Configuration configuration, Database database, FreenetInterface freenetInterface, IdentityManager identityManager) {
 -      public Core(Configuration configuration, FreenetInterface freenetInterface, IdentityManager identityManager, WebOfTrustUpdater webOfTrustUpdater) {
++      public Core(Configuration configuration, Database database, FreenetInterface freenetInterface, IdentityManager identityManager, WebOfTrustUpdater webOfTrustUpdater) {
                super("Sone Core");
                this.configuration = configuration;
 +              this.database = database;
                this.freenetInterface = freenetInterface;
                this.identityManager = identityManager;
                this.soneDownloader = new SoneDownloader(this, freenetInterface);
                this.imageInserter = new ImageInserter(this, freenetInterface);
                this.updateChecker = new UpdateChecker(freenetInterface);
+               this.webOfTrustUpdater = webOfTrustUpdater;
        }
  
        //
         * @return The Sone rescuer for the given Sone
         */
        public SoneRescuer getSoneRescuer(Sone sone) {
 -              Validation.begin().isNotNull("Sone", sone).check().is("Local Sone", isLocalSone(sone)).check();
 -              synchronized (localSones) {
 +              Validation.begin().isNotNull("Sone", sone).check().is("Local Sone", sone.isLocal()).check();
 +              synchronized (soneRescuers) {
                        SoneRescuer soneRescuer = soneRescuers.get(sone);
                        if (soneRescuer == null) {
                                soneRescuer = new SoneRescuer(this, soneDownloader, sone);
         *
         * @return All Sones
         */
 -      public Set<Sone> getSones() {
 -              Set<Sone> allSones = new HashSet<Sone>();
 -              allSones.addAll(getLocalSones());
 -              allSones.addAll(getRemoteSones());
 -              return allSones;
 +      public Collection<Sone> getSones() {
 +              return database.getSones();
        }
  
        /**
         */
        @Override
        public Sone getSone(String id, boolean create) {
 -              if (isLocalSone(id)) {
 -                      return getLocalSone(id);
 -              }
 -              return getRemoteSone(id, create);
 +              return database.getSone(id, create);
        }
  
        /**
         *         otherwise
         */
        public boolean hasSone(String id) {
 -              return isLocalSone(id) || isRemoteSone(id);
 -      }
 -
 -      /**
 -       * Returns whether the given Sone is a local Sone.
 -       *
 -       * @param sone
 -       *            The Sone to check for its locality
 -       * @return {@code true} if the given Sone is local, {@code false} otherwise
 -       */
 -      public boolean isLocalSone(Sone sone) {
 -              synchronized (localSones) {
 -                      return localSones.containsKey(sone.getId());
 -              }
 -      }
 -
 -      /**
 -       * Returns whether the given ID is the ID of a local Sone.
 -       *
 -       * @param id
 -       *            The Sone ID to check for its locality
 -       * @return {@code true} if the given ID is a local Sone, {@code false}
 -       *         otherwise
 -       */
 -      public boolean isLocalSone(String id) {
 -              synchronized (localSones) {
 -                      return localSones.containsKey(id);
 -              }
 +              return database.getSone(id, false) != null;
        }
  
        /**
         *
         * @return All local Sones
         */
 -      public Set<Sone> getLocalSones() {
 -              synchronized (localSones) {
 -                      return new HashSet<Sone>(localSones.values());
 -              }
 +      public Collection<Sone> getLocalSones() {
 +              return database.getLocalSones();
        }
  
        /**
         * @return The Sone with the given ID, or {@code null}
         */
        public Sone getLocalSone(String id, boolean create) {
 -              synchronized (localSones) {
 -                      Sone sone = localSones.get(id);
 -                      if ((sone == null) && create) {
 -                              sone = new Sone(id);
 -                              localSones.put(id, sone);
 -                      }
 -                      return sone;
 -              }
 +              return database.getLocalSone(id, create);
        }
  
        /**
         *
         * @return All remote Sones
         */
 -      public Set<Sone> getRemoteSones() {
 -              synchronized (remoteSones) {
 -                      return new HashSet<Sone>(remoteSones.values());
 -              }
 +      public Collection<Sone> getRemoteSones() {
 +              return database.getRemoteSones();
        }
  
        /**
         * @return The Sone with the given ID
         */
        public Sone getRemoteSone(String id, boolean create) {
 -              synchronized (remoteSones) {
 -                      Sone sone = remoteSones.get(id);
 -                      if ((sone == null) && create && (id != null) && (id.length() == 43)) {
 -                              sone = new Sone(id);
 -                              remoteSones.put(id, sone);
 -                      }
 -                      return sone;
 -              }
 -      }
 -
 -      /**
 -       * Returns whether the given Sone is a remote Sone.
 -       *
 -       * @param sone
 -       *            The Sone to check
 -       * @return {@code true} if the given Sone is a remote Sone, {@code false}
 -       *         otherwise
 -       */
 -      public boolean isRemoteSone(Sone sone) {
 -              synchronized (remoteSones) {
 -                      return remoteSones.containsKey(sone.getId());
 -              }
 -      }
 -
 -      /**
 -       * Returns whether the Sone with the given ID is a remote Sone.
 -       *
 -       * @param id
 -       *            The ID of the Sone to check
 -       * @return {@code true} if the Sone with the given ID is a remote Sone,
 -       *         {@code false} otherwise
 -       */
 -      public boolean isRemoteSone(String id) {
 -              synchronized (remoteSones) {
 -                      return remoteSones.containsKey(id);
 -              }
 +              return database.getRemoteSone(id, create);
        }
  
        /**
        }
  
        /**
 -       * Returns the post with the given ID.
 -       *
 -       * @param postId
 -       *            The ID of the post to get
 -       * @return The post with the given ID, or a new post with the given ID
 -       */
 -      public Post getPost(String postId) {
 -              return getPost(postId, true);
 -      }
 -
 -      /**
         * Returns the post with the given ID, optionally creating a new post.
         *
         * @param postId
                synchronized (posts) {
                        Post post = posts.get(postId);
                        if ((post == null) && create) {
-                               post = new Post(postId);
+                               post = new PostImpl(postId);
                                posts.put(postId, post);
                        }
                        return post;
         * @return All replies for the given post
         */
        public List<PostReply> getReplies(Post post) {
 -              Set<Sone> sones = getSones();
 +              Collection<Sone> sones = getSones();
                List<PostReply> replies = new ArrayList<PostReply>();
                for (Sone sone : sones) {
                        for (PostReply reply : sone.getReplies()) {
        }
  
        /**
- <<<<<<< HEAD
- =======
-        * Adds a local Sone from the given ID which has to be the ID of an own
-        * identity.
-        *
-        * @param id
-        *            The ID of an own identity to add a Sone for
-        * @return The added (or already existing) Sone
-        */
-       public Sone addLocalSone(String id) {
-               if (database.getLocalSone(id, false) != null) {
-                       logger.log(Level.FINE, "Tried to add known local Sone: %s", id);
-                       return database.getLocalSone(id, false);
-               }
-               OwnIdentity ownIdentity = identityManager.getOwnIdentity(id);
-               if (ownIdentity == null) {
-                       logger.log(Level.INFO, "Invalid Sone ID: %s", id);
-                       return null;
-               }
-               return addLocalSone(ownIdentity);
-       }
-       /**
- >>>>>>> 4f36598... Store locality of a Sone in the Sone itself, remove related methods from Database.
         * Adds a local Sone from the given own identity.
         *
         * @param ownIdentity
                        logger.log(Level.WARNING, "Given OwnIdentity is null!");
                        return null;
                }
 -              synchronized (localSones) {
 -                      final Sone sone;
 -                      try {
 -                              sone = getLocalSone(ownIdentity.getId()).setIdentity(ownIdentity).setInsertUri(new FreenetURI(ownIdentity.getInsertUri())).setRequestUri(new FreenetURI(ownIdentity.getRequestUri()));
 -                      } catch (MalformedURLException mue1) {
 -                              logger.log(Level.SEVERE, String.format("Could not convert the Identity’s URIs to Freenet URIs: %s, %s", ownIdentity.getInsertUri(), ownIdentity.getRequestUri()), mue1);
 -                              return null;
 -                      }
 -                      sone.setLatestEdition(Numbers.safeParseLong(ownIdentity.getProperty("Sone.LatestEdition"), (long) 0));
 -                      sone.setClient(new Client("Sone", SonePlugin.VERSION.toString()));
 -                      sone.setKnown(true);
 -                      /* TODO - load posts ’n stuff */
 -                      localSones.put(ownIdentity.getId(), sone);
 -                      final SoneInserter soneInserter = new SoneInserter(this, freenetInterface, sone);
 -                      soneInserter.addSoneInsertListener(this);
 -                      soneInserters.put(sone, soneInserter);
 -                      sone.setStatus(SoneStatus.idle);
 -                      loadSone(sone);
 -                      soneInserter.start();
 -                      return sone;
 +              Sone sone;
 +              try {
 +                      sone = getLocalSone(ownIdentity.getId()).setIdentity(ownIdentity).setInsertUri(new FreenetURI(ownIdentity.getInsertUri())).setRequestUri(new FreenetURI(ownIdentity.getRequestUri()));
 +              } catch (MalformedURLException mue1) {
 +                      logger.log(Level.SEVERE, String.format("Could not convert the Identity’s URIs to Freenet URIs: %s, %s", ownIdentity.getInsertUri(), ownIdentity.getRequestUri()), mue1);
 +                      return null;
                }
 +              sone.setLatestEdition(Numbers.safeParseLong(ownIdentity.getProperty("Sone.LatestEdition"), (long) 0));
 +              sone.setClient(new Client("Sone", SonePlugin.VERSION.toString()));
 +              sone.setKnown(true);
 +              /* TODO - load posts ’n stuff */
 +              final SoneInserter soneInserter = new SoneInserter(this, freenetInterface, sone);
 +              soneInserter.addSoneInsertListener(this);
 +              soneInserters.put(sone, soneInserter);
 +              sone.setStatus(SoneStatus.idle);
 +              loadSone(sone);
 +              soneInserter.start();
 +              database.saveSone(sone);
 +              return sone;
        }
  
        /**
         * @return The created Sone
         */
        public Sone createSone(OwnIdentity ownIdentity) {
-               try {
-                       ownIdentity.addContext("Sone");
-               } catch (WebOfTrustException wote1) {
-                       logger.log(Level.SEVERE, String.format("Could not add “Sone” context to own identity: %s", ownIdentity), wote1);
+               if (!webOfTrustUpdater.addContextWait(ownIdentity, "Sone")) {
+                       logger.log(Level.SEVERE, String.format("Could not add “Sone” context to own identity: %s", ownIdentity));
                        return null;
                }
                Sone sone = addLocalSone(ownIdentity);
                        logger.log(Level.WARNING, "Given Identity is null!");
                        return null;
                }
 -              synchronized (remoteSones) {
 -                      final Sone sone = getRemoteSone(identity.getId(), true).setIdentity(identity);
 -                      boolean newSone = sone.getRequestUri() == null;
 -                      sone.setRequestUri(getSoneUri(identity.getRequestUri()));
 -                      sone.setLatestEdition(Numbers.safeParseLong(identity.getProperty("Sone.LatestEdition"), (long) 0));
 +              final Sone sone = getRemoteSone(identity.getId(), true).setIdentity(identity);
 +              boolean newSone = sone.getRequestUri() == null;
 +              sone.setRequestUri(getSoneUri(identity.getRequestUri()));
 +              sone.setLatestEdition(Numbers.safeParseLong(identity.getProperty("Sone.LatestEdition"), (long) 0));
 +              if (newSone) {
 +                      synchronized (knownSones) {
 +                              newSone = !knownSones.contains(sone.getId());
 +                      }
 +                      sone.setKnown(!newSone);
                        if (newSone) {
 -                              synchronized (knownSones) {
 -                                      newSone = !knownSones.contains(sone.getId());
 -                              }
 -                              sone.setKnown(!newSone);
 -                              if (newSone) {
 -                                      coreListenerManager.fireNewSoneFound(sone);
 -                                      for (Sone localSone : getLocalSones()) {
 -                                              if (localSone.getOptions().getBooleanOption("AutoFollow").get()) {
 -                                                      followSone(localSone, sone);
 -                                              }
 +                              coreListenerManager.fireNewSoneFound(sone);
 +                              for (Sone localSone : getLocalSones()) {
 +                                      if (localSone.getOptions().getBooleanOption("AutoFollow").get()) {
 +                                              followSone(localSone, sone);
                                        }
                                }
                        }
 -                      soneDownloader.addSone(sone);
 -                      soneDownloaders.execute(new Runnable() {
 +              }
 +              soneDownloader.addSone(sone);
 +              soneDownloaders.execute(new Runnable() {
  
 -                              @Override
 -                              @SuppressWarnings("synthetic-access")
 -                              public void run() {
 -                                      soneDownloader.fetchSone(sone, sone.getRequestUri());
 -                              }
 +                      @Override
 +                      @SuppressWarnings("synthetic-access")
 +                      public void run() {
 +                              soneDownloader.fetchSone(sone, sone.getRequestUri());
 +                      }
  
 -                      });
 -                      return sone;
 -              }
 +              });
 +              database.saveSone(sone);
 +              return sone;
        }
  
        /**
        }
  
        /**
-        * Retrieves the trust relationship from the origin to the target. If the
-        * trust relationship can not be retrieved, {@code null} is returned.
-        *
-        * @see Identity#getTrust(OwnIdentity)
-        * @param origin
-        *            The origin of the trust tree
-        * @param target
-        *            The target of the trust
-        * @return The trust relationship
-        */
-       public Trust getTrust(Sone origin, Sone target) {
-               if (!origin.isLocal()) {
-                       logger.log(Level.WARNING, String.format("Tried to get trust from remote Sone: %s", origin));
-                       return null;
-               }
-               return target.getIdentity().getTrust((OwnIdentity) origin.getIdentity());
-       }
-       /**
--       * Sets the trust value of the given origin Sone for the target Sone.
++       * Sets the trust value of the given origin Sone for
++       * the target Sone.
         *
         * @param origin
         *            The origin Sone
         */
        public void setTrust(Sone origin, Sone target, int trustValue) {
                Validation.begin().isNotNull("Trust Origin", origin).check().isInstanceOf("Trust Origin", origin.getIdentity(), OwnIdentity.class).isNotNull("Trust Target", target).isLessOrEqual("Trust Value", trustValue, 100).isGreaterOrEqual("Trust Value", trustValue, -100).check();
-               try {
-                       ((OwnIdentity) origin.getIdentity()).setTrust(target.getIdentity(), trustValue, preferences.getTrustComment());
-               } catch (WebOfTrustException wote1) {
-                       logger.log(Level.WARNING, String.format("Could not set trust for Sone: %s", target), wote1);
-               }
+               webOfTrustUpdater.setTrust((OwnIdentity) origin.getIdentity(), target.getIdentity(), trustValue, preferences.getTrustComment());
        }
  
        /**
         */
        public void removeTrust(Sone origin, Sone target) {
                Validation.begin().isNotNull("Trust Origin", origin).isNotNull("Trust Target", target).check().isInstanceOf("Trust Origin Identity", origin.getIdentity(), OwnIdentity.class).check();
-               try {
-                       ((OwnIdentity) origin.getIdentity()).removeTrust(target.getIdentity());
-               } catch (WebOfTrustException wote1) {
-                       logger.log(Level.WARNING, String.format("Could not remove trust for Sone: %s", target), wote1);
-               }
+               webOfTrustUpdater.setTrust((OwnIdentity) origin.getIdentity(), target.getIdentity(), null, null);
        }
  
        /**
                        logger.log(Level.WARNING, String.format("Tried to delete Sone of non-own identity: %s", sone));
                        return;
                }
 -              synchronized (localSones) {
 -                      if (!localSones.containsKey(sone.getId())) {
 -                              logger.log(Level.WARNING, String.format("Tried to delete non-local Sone: %s", sone));
 -                              return;
 -                      }
 -                      localSones.remove(sone.getId());
 -                      SoneInserter soneInserter = soneInserters.remove(sone);
 -                      soneInserter.removeSoneInsertListener(this);
 -                      soneInserter.stop();
 +              if (!sone.isLocal()) {
 +                      logger.log(Level.WARNING, String.format("Tried to delete non-local Sone: %s", sone));
 +                      return;
                }
-               try {
-                       ((OwnIdentity) sone.getIdentity()).removeContext("Sone");
-                       ((OwnIdentity) sone.getIdentity()).removeProperty("Sone.LatestEdition");
-               } catch (WebOfTrustException wote1) {
-                       logger.log(Level.WARNING, String.format("Could not remove context and properties from Sone: %s", sone), wote1);
-               }
 +              database.removeSone(sone.getId());
 +              SoneInserter soneInserter = soneInserters.remove(sone);
 +              soneInserter.removeSoneInsertListener(this);
 +              soneInserter.stop();
+               webOfTrustUpdater.removeContext((OwnIdentity) sone.getIdentity(), "Sone");
+               webOfTrustUpdater.removeProperty((OwnIdentity) sone.getIdentity(), "Sone.LatestEdition");
                try {
                        configuration.getLongValue("Sone/" + sone.getId() + "/Time").setValue(null);
                } catch (ConfigurationException ce1) {
         *            The Sone to load and update
         */
        public void loadSone(Sone sone) {
 -              if (!isLocalSone(sone)) {
 +              if (!sone.isLocal()) {
                        logger.log(Level.FINE, String.format("Tried to load non-local Sone: %s", sone));
                        return;
                }
                                logger.log(Level.WARNING, "Invalid post found, aborting load!");
                                return;
                        }
 -                      Post post = getPost(postId).setSone(sone).setTime(postTime).setText(postText);
 +                      Post post = getPost(postId, true).setSone(sone).setTime(postTime).setText(postText);
                        if ((postRecipientId != null) && (postRecipientId.length() == 43)) {
                                post.setRecipient(getSone(postRecipientId));
                        }
                                logger.log(Level.WARNING, "Invalid reply found, aborting load!");
                                return;
                        }
 -                      replies.add(getReply(replyId).setSone(sone).setPost(getPost(postId)).setTime(replyTime).setText(replyText));
 +                      replies.add(getReply(replyId).setSone(sone).setPost(getPost(postId, true)).setTime(replyTime).setText(replyText));
                }
  
                /* load post likes. */
         *
         * @param sone
         *            The Sone that creates the post
 -       * @param text
 -       *            The text of the post
 -       * @return The created post
 -       */
 -      public Post createPost(Sone sone, String text) {
 -              return createPost(sone, System.currentTimeMillis(), text);
 -      }
 -
 -      /**
 -       * Creates a new post.
 -       *
 -       * @param sone
 -       *            The Sone that creates the post
 -       * @param time
 -       *            The time of the post
 -       * @param text
 -       *            The text of the post
 -       * @return The created post
 -       */
 -      public Post createPost(Sone sone, long time, String text) {
 -              return createPost(sone, null, time, text);
 -      }
 -
 -      /**
 -       * Creates a new post.
 -       *
 -       * @param sone
 -       *            The Sone that creates the post
 -       * @param recipient
 -       *            The recipient Sone, or {@code null} if this post does not have
 -       *            a recipient
 -       * @param text
 -       *            The text of the post
 -       * @return The created post
 -       */
 -      public Post createPost(Sone sone, 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
         * @return The created post
         */
        public Post createPost(Sone sone, Sone recipient, long time, String text) {
 -              if (!isLocalSone(sone)) {
 +              if (!sone.isLocal()) {
                        logger.log(Level.FINE, String.format("Tried to create post for non-local Sone: %s", sone));
                        return null;
                }
-               final Post post = new Post(sone, time, text);
+               final Post post = new PostImpl(sone, time, text);
                if (recipient != null) {
                        post.setRecipient(recipient);
                }
         *            The post to delete
         */
        public void deletePost(Post post) {
 -              if (!isLocalSone(post.getSone())) {
 +              if (!post.getSone().isLocal()) {
                        logger.log(Level.WARNING, String.format("Tried to delete post of non-local Sone: %s", post.getSone()));
                        return;
                }
         * @return The created reply
         */
        public PostReply createReply(Sone sone, Post post, long time, String text) {
 -              if (!isLocalSone(sone)) {
 +              if (!sone.isLocal()) {
                        logger.log(Level.FINE, String.format("Tried to create reply for non-local Sone: %s", sone));
                        return null;
                }
         */
        public void deleteReply(PostReply reply) {
                Sone sone = reply.getSone();
 -              if (!isLocalSone(sone)) {
 +              if (!sone.isLocal()) {
                        logger.log(Level.FINE, String.format("Tried to delete non-local reply: %s", reply));
                        return;
                }
         *            The album to remove
         */
        public void deleteAlbum(Album album) {
 -              Validation.begin().isNotNull("Album", album).check().is("Local Sone", isLocalSone(album.getSone())).check();
 +              Validation.begin().isNotNull("Album", album).check().is("Local Sone", album.getSone().isLocal()).check();
                if (!album.isEmpty()) {
                        return;
                }
                synchronized (albums) {
                        albums.remove(album.getId());
                }
-               saveSone(album.getSone());
+               touchConfiguration();
        }
  
        /**
         * @return The newly created image
         */
        public Image createImage(Sone sone, Album album, TemporaryImage temporaryImage) {
 -              Validation.begin().isNotNull("Sone", sone).isNotNull("Album", album).isNotNull("Temporary Image", temporaryImage).check().is("Local Sone", isLocalSone(sone)).check().isEqual("Owner and Album Owner", sone, album.getSone()).check();
 +              Validation.begin().isNotNull("Sone", sone).isNotNull("Album", album).isNotNull("Temporary Image", temporaryImage).check().is("Local Sone", sone.isLocal()).check().isEqual("Owner and Album Owner", sone, album.getSone()).check();
                Image image = new Image(temporaryImage.getId()).setSone(sone).setCreationTime(System.currentTimeMillis());
                album.addImage(image);
                synchronized (images) {
         *            The image to delete
         */
        public void deleteImage(Image image) {
 -              Validation.begin().isNotNull("Image", image).check().is("Local Sone", isLocalSone(image.getSone())).check();
 +              Validation.begin().isNotNull("Image", image).check().is("Local Sone", image.getSone().isLocal()).check();
                deleteTemporaryImage(image.getId());
                image.getAlbum().removeImage(image);
                synchronized (images) {
                        images.remove(image.getId());
                }
-               saveSone(image.getSone());
+               touchConfiguration();
        }
  
        /**
                loadConfiguration();
                updateChecker.addUpdateListener(this);
                updateChecker.start();
+               webOfTrustUpdater.start();
        }
  
        /**
         */
        @Override
        public void serviceStop() {
-               for (SoneInserter soneInserter : soneInserters.values()) {
-                       soneInserter.removeSoneInsertListener(this);
-                       soneInserter.stop();
 -              synchronized (localSones) {
 -                      for (Entry<Sone, SoneInserter> soneInserter : soneInserters.entrySet()) {
 -                              soneInserter.getValue().removeSoneInsertListener(this);
 -                              soneInserter.getValue().stop();
 -                              saveSone(soneInserter.getKey());
 -                      }
++              for (Entry<Sone, SoneInserter> soneInserter : soneInserters.entrySet()) {
++                      soneInserter.getValue().removeSoneInsertListener(this);
++                      soneInserter.getValue().stop();
++                      saveSone(soneInserter.getKey());
                }
+               saveConfiguration();
+               webOfTrustUpdater.stop();
                updateChecker.stop();
                updateChecker.removeUpdateListener(this);
                soneDownloader.stop();
         *            The Sone to save
         */
        private synchronized void saveSone(Sone sone) {
 -              if (!isLocalSone(sone)) {
 +              if (!sone.isLocal()) {
                        logger.log(Level.FINE, String.format("Tried to save non-local Sone: %s", sone));
                        return;
                }
  
                        configuration.save();
  
-                       ((OwnIdentity) sone.getIdentity()).setProperty("Sone.LatestEdition", String.valueOf(sone.getLatestEdition()));
+                       webOfTrustUpdater.setProperty((OwnIdentity) sone.getIdentity(), "Sone.LatestEdition", String.valueOf(sone.getLatestEdition()));
  
                        logger.log(Level.INFO, String.format("Sone %s saved.", sone));
                } catch (ConfigurationException ce1) {
                        logger.log(Level.WARNING, String.format("Could not save Sone: %s", sone), ce1);
-               } catch (WebOfTrustException wote1) {
-                       logger.log(Level.WARNING, String.format("Could not set WoT property for Sone: %s", sone), wote1);
                }
        }
  
                        configuration.getIntValue("Option/ConfigurationVersion").setValue(0);
                        configuration.getIntValue("Option/InsertionDelay").setValue(options.getIntegerOption("InsertionDelay").getReal());
                        configuration.getIntValue("Option/PostsPerPage").setValue(options.getIntegerOption("PostsPerPage").getReal());
+                       configuration.getIntValue("Option/ImagesPerPage").setValue(options.getIntegerOption("ImagesPerPage").getReal());
                        configuration.getIntValue("Option/CharactersPerPost").setValue(options.getIntegerOption("CharactersPerPost").getReal());
                        configuration.getIntValue("Option/PostCutOffLength").setValue(options.getIntegerOption("PostCutOffLength").getReal());
                        configuration.getBooleanValue("Option/RequireFullAccess").setValue(options.getBooleanOption("RequireFullAccess").getReal());
                        configuration.getStringValue("Option/TrustComment").setValue(options.getStringOption("TrustComment").getReal());
                        configuration.getBooleanValue("Option/ActivateFcpInterface").setValue(options.getBooleanOption("ActivateFcpInterface").getReal());
                        configuration.getIntValue("Option/FcpFullAccessRequired").setValue(options.getIntegerOption("FcpFullAccessRequired").getReal());
-                       configuration.getBooleanValue("Option/SoneRescueMode").setValue(options.getBooleanOption("SoneRescueMode").getReal());
-                       configuration.getBooleanValue("Option/ClearOnNextRestart").setValue(options.getBooleanOption("ClearOnNextRestart").getReal());
-                       configuration.getBooleanValue("Option/ReallyClearOnNextRestart").setValue(options.getBooleanOption("ReallyClearOnNextRestart").getReal());
  
                        /* save known Sones. */
                        int soneCounter = 0;
  
                }));
                options.addIntegerOption("PostsPerPage", new DefaultOption<Integer>(10, new IntegerRangeValidator(1, Integer.MAX_VALUE)));
+               options.addIntegerOption("ImagesPerPage", new DefaultOption<Integer>(9, new IntegerRangeValidator(1, Integer.MAX_VALUE)));
                options.addIntegerOption("CharactersPerPost", new DefaultOption<Integer>(400, new OrValidator<Integer>(new IntegerRangeValidator(50, Integer.MAX_VALUE), new EqualityValidator<Integer>(-1))));
                options.addIntegerOption("PostCutOffLength", new DefaultOption<Integer>(200, new OrValidator<Integer>(new IntegerRangeValidator(50, Integer.MAX_VALUE), new EqualityValidator<Integer>(-1))));
                options.addBooleanOption("RequireFullAccess", new DefaultOption<Boolean>(false));
                        }
  
                }));
-               options.addBooleanOption("SoneRescueMode", new DefaultOption<Boolean>(false));
-               options.addBooleanOption("ClearOnNextRestart", new DefaultOption<Boolean>(false));
-               options.addBooleanOption("ReallyClearOnNextRestart", new DefaultOption<Boolean>(false));
-               /* read options from configuration. */
-               options.getBooleanOption("ClearOnNextRestart").set(configuration.getBooleanValue("Option/ClearOnNextRestart").getValue(null));
-               options.getBooleanOption("ReallyClearOnNextRestart").set(configuration.getBooleanValue("Option/ReallyClearOnNextRestart").getValue(null));
-               boolean clearConfiguration = options.getBooleanOption("ClearOnNextRestart").get() && options.getBooleanOption("ReallyClearOnNextRestart").get();
-               options.getBooleanOption("ClearOnNextRestart").set(null);
-               options.getBooleanOption("ReallyClearOnNextRestart").set(null);
-               if (clearConfiguration) {
-                       /* stop loading the configuration. */
-                       return;
-               }
  
                loadConfigurationValue("InsertionDelay");
                loadConfigurationValue("PostsPerPage");
+               loadConfigurationValue("ImagesPerPage");
                loadConfigurationValue("CharactersPerPost");
                loadConfigurationValue("PostCutOffLength");
                options.getBooleanOption("RequireFullAccess").set(configuration.getBooleanValue("Option/RequireFullAccess").getValue(null));
                options.getStringOption("TrustComment").set(configuration.getStringValue("Option/TrustComment").getValue(null));
                options.getBooleanOption("ActivateFcpInterface").set(configuration.getBooleanValue("Option/ActivateFcpInterface").getValue(null));
                options.getIntegerOption("FcpFullAccessRequired").set(configuration.getIntValue("Option/FcpFullAccessRequired").getValue(null));
-               options.getBooleanOption("SoneRescueMode").set(configuration.getBooleanValue("Option/SoneRescueMode").getValue(null));
  
                /* load known Sones. */
                int soneCounter = 0;
         *            The URI to derive the Sone URI from
         * @return The derived URI
         */
-       private FreenetURI getSoneUri(String uriString) {
+       private static FreenetURI getSoneUri(String uriString) {
                try {
                        FreenetURI uri = new FreenetURI(uriString).setDocName("Sone").setMetaString(new String[0]);
                        return uri;
                } catch (MalformedURLException mue1) {
-                       logger.log(Level.WARNING, String.format("Could not create Sone URI from URI: %s", uriString, mue1));
+                       logger.log(Level.WARNING, String.format("Could not create Sone URI from URI: %s", uriString), mue1);
                        return null;
                }
        }
                                }
                        }
                }
 -              synchronized (remoteSones) {
 -                      remoteSones.remove(identity.getId());
 -              }
 +              database.removeSone(identity.getId());
                coreListenerManager.fireSoneRemoved(sone);
        }
  
                logger.log(Level.WARNING, String.format("Image insert finished for %s: %s", image, key));
                image.setKey(key.toString());
                deleteTemporaryImage(image.getId());
-               saveSone(image.getSone());
+               touchConfiguration();
                coreListenerManager.fireImageInsertFinished(image);
        }
  
                }
  
                /**
+                * Returns the number of images to show per page.
+                *
+                * @return The number of images to show per page
+                */
+               public int getImagesPerPage() {
+                       return options.getIntegerOption("ImagesPerPage").get();
+               }
+               /**
+                * Validates the number of images per page.
+                *
+                * @param imagesPerPage
+                *            The number of images per page
+                * @return {@code true} if the number of images per page was valid,
+                *         {@code false} otherwise
+                */
+               public boolean validateImagesPerPage(Integer imagesPerPage) {
+                       return options.getIntegerOption("ImagesPerPage").validate(imagesPerPage);
+               }
+               /**
+                * Sets the number of images per page.
+                *
+                * @param imagesPerPage
+                *            The number of images per page
+                * @return This preferences object
+                */
+               public Preferences setImagesPerPage(Integer imagesPerPage) {
+                       options.getIntegerOption("ImagesPerPage").set(imagesPerPage);
+                       return this;
+               }
+               /**
                 * Returns the number of characters per post, or <code>-1</code> if the
                 * posts should not be cut off.
                 *
                        return this;
                }
  
-               /**
-                * Returns whether Sone should clear its settings on the next restart.
-                * In order to be effective, {@link #isReallyClearOnNextRestart()} needs
-                * to return {@code true} as well!
-                *
-                * @return {@code true} if Sone should clear its settings on the next
-                *         restart, {@code false} otherwise
-                */
-               public boolean isClearOnNextRestart() {
-                       return options.getBooleanOption("ClearOnNextRestart").get();
-               }
-               /**
-                * Sets whether Sone will clear its settings on the next restart.
-                *
-                * @param clearOnNextRestart
-                *            {@code true} if Sone should clear its settings on the next
-                *            restart, {@code false} otherwise
-                * @return This preferences
-                */
-               public Preferences setClearOnNextRestart(Boolean clearOnNextRestart) {
-                       options.getBooleanOption("ClearOnNextRestart").set(clearOnNextRestart);
-                       return this;
-               }
-               /**
-                * Returns whether Sone should really clear its settings on next
-                * restart. This is a confirmation option that needs to be set in
-                * addition to {@link #isClearOnNextRestart()} in order to clear Sone’s
-                * settings on the next restart.
-                *
-                * @return {@code true} if Sone should really clear its settings on the
-                *         next restart, {@code false} otherwise
-                */
-               public boolean isReallyClearOnNextRestart() {
-                       return options.getBooleanOption("ReallyClearOnNextRestart").get();
-               }
-               /**
-                * Sets whether Sone should really clear its settings on the next
-                * restart.
-                *
-                * @param reallyClearOnNextRestart
-                *            {@code true} if Sone should really clear its settings on
-                *            the next restart, {@code false} otherwise
-                * @return This preferences
-                */
-               public Preferences setReallyClearOnNextRestart(Boolean reallyClearOnNextRestart) {
-                       options.getBooleanOption("ReallyClearOnNextRestart").set(reallyClearOnNextRestart);
-                       return this;
-               }
        }
  
  }
index 1c5912f,0000000..da487b3
mode 100644,000000..100644
--- /dev/null
@@@ -1,735 -1,0 +1,737 @@@
-               albums.add(album);
 +/*
 + * Sone - MemorySone.java - Copyright © 2010–2012 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
 + * the Free Software Foundation, either version 3 of the License, or
 + * (at your option) any later version.
 + *
 + * This program is distributed in the hope that it will be useful,
 + * but WITHOUT ANY WARRANTY; without even the implied warranty of
 + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 + * GNU General Public License for more details.
 + *
 + * You should have received a copy of the GNU General Public License
 + * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 + */
 +
 +package net.pterodactylus.sone.database.memory;
 +
 +import java.util.ArrayList;
 +import java.util.Collection;
 +import java.util.Collections;
 +import java.util.List;
 +import java.util.Set;
 +import java.util.concurrent.CopyOnWriteArrayList;
 +import java.util.concurrent.CopyOnWriteArraySet;
 +import java.util.logging.Level;
 +import java.util.logging.Logger;
 +
 +import net.pterodactylus.sone.core.Options;
 +import net.pterodactylus.sone.data.Album;
 +import net.pterodactylus.sone.data.Client;
 +import net.pterodactylus.sone.data.Image;
 +import net.pterodactylus.sone.data.Post;
 +import net.pterodactylus.sone.data.PostReply;
 +import net.pterodactylus.sone.data.Profile;
 +import net.pterodactylus.sone.data.Reply;
 +import net.pterodactylus.sone.data.Sone;
 +import net.pterodactylus.sone.database.Database;
 +import net.pterodactylus.sone.freenet.wot.Identity;
 +import net.pterodactylus.util.logging.Logging;
 +import net.pterodactylus.util.validation.Validation;
 +import freenet.keys.FreenetURI;
 +
 +/**
 + * Implementation of a {@link Sone} that keeps all added data in memory. A
 + * self-created instance of this class should be converted to a {@link Database}
 + * -based instance of {@link Sone} as soon as possible (unless it was returned
 + * by a {@link MemoryDatabase}).
 + *
 + * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
 + */
 +public class MemorySone implements Sone {
 +
 +      /** The logger. */
 +      private static final Logger logger = Logging.getLogger(Sone.class);
 +
 +      /** The ID of this Sone. */
 +      private final String id;
 +
 +      /** Whether this is a local Sone. */
 +      private final boolean local;
 +
 +      /** The identity of this Sone. */
 +      private Identity identity;
 +
 +      /** The URI under which the Sone is stored in Freenet. */
 +      private volatile FreenetURI requestUri;
 +
 +      /** The URI used to insert a new version of this Sone. */
 +      /* This will be null for remote Sones! */
 +      private volatile FreenetURI insertUri;
 +
 +      /** The latest edition of the Sone. */
 +      private volatile long latestEdition;
 +
 +      /** The time of the last inserted update. */
 +      private volatile long time;
 +
 +      /** The status of this Sone. */
 +      private volatile SoneStatus status = SoneStatus.unknown;
 +
 +      /** The profile of this Sone. */
 +      private volatile Profile profile = new Profile(this);
 +
 +      /** The client used by the Sone. */
 +      private volatile Client client;
 +
 +      /** Whether this Sone is known. */
 +      private volatile boolean known;
 +
 +      /** All friend Sones. */
 +      private final Set<String> friendSones = new CopyOnWriteArraySet<String>();
 +
 +      /** All posts. */
 +      private final Set<Post> posts = new CopyOnWriteArraySet<Post>();
 +
 +      /** All replies. */
 +      private final Set<PostReply> replies = new CopyOnWriteArraySet<PostReply>();
 +
 +      /** The IDs of all liked posts. */
 +      private final Set<String> likedPostIds = new CopyOnWriteArraySet<String>();
 +
 +      /** The IDs of all liked replies. */
 +      private final Set<String> likedReplyIds = new CopyOnWriteArraySet<String>();
 +
 +      /** The albums of this Sone. */
 +      private final List<Album> albums = new CopyOnWriteArrayList<Album>();
 +
 +      /** Sone-specific options. */
 +      private final Options options = new Options();
 +
 +      /**
 +       * Creates a new Sone.
 +       *
 +       * @param id
 +       *            The ID of the Sone
 +       * @param local
 +       *            {@code true} if this Sone is local, {@code false} otherwise
 +       */
 +      public MemorySone(String id, boolean local) {
 +              this.id = id;
 +              this.local = local;
 +      }
 +
 +      //
 +      // ACCESSORS
 +      //
 +
 +      /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public String getId() {
 +              return id;
 +      }
 +
 +      /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public Identity getIdentity() {
 +              return identity;
 +      }
 +
 +      /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public Sone setIdentity(Identity identity) throws IllegalArgumentException {
 +              if (!identity.getId().equals(id)) {
 +                      throw new IllegalArgumentException("Identity’s ID does not match Sone’s ID!");
 +              }
 +              this.identity = identity;
 +              return this;
 +      }
 +
 +      /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public boolean isLocal() {
 +              return local;
 +      }
 +
 +      /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public String getName() {
 +              return (identity != null) ? identity.getNickname() : null;
 +      }
 +
 +      /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public FreenetURI getRequestUri() {
 +              return (requestUri != null) ? requestUri.setSuggestedEdition(latestEdition) : null;
 +      }
 +
 +      /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public Sone setRequestUri(FreenetURI requestUri) {
 +              if (this.requestUri == null) {
 +                      this.requestUri = requestUri.setKeyType("USK").setDocName("Sone").setMetaString(new String[0]);
 +                      return this;
 +              }
 +              if (!this.requestUri.equalsKeypair(requestUri)) {
 +                      logger.log(Level.WARNING, String.format("Request URI %s tried to overwrite %s!", requestUri, this.requestUri));
 +                      return this;
 +              }
 +              return this;
 +      }
 +
 +      /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public FreenetURI getInsertUri() {
 +              return (insertUri != null) ? insertUri.setSuggestedEdition(latestEdition) : null;
 +      }
 +
 +      /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public Sone setInsertUri(FreenetURI insertUri) {
 +              if (this.insertUri == null) {
 +                      this.insertUri = insertUri.setKeyType("USK").setDocName("Sone").setMetaString(new String[0]);
 +                      return this;
 +              }
 +              if (!this.insertUri.equalsKeypair(insertUri)) {
 +                      logger.log(Level.WARNING, String.format("Request URI %s tried to overwrite %s!", insertUri, this.insertUri));
 +                      return this;
 +              }
 +              return this;
 +      }
 +
 +      /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public long getLatestEdition() {
 +              return latestEdition;
 +      }
 +
 +      /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public void setLatestEdition(long latestEdition) {
 +              if (!(latestEdition > this.latestEdition)) {
 +                      logger.log(Level.FINE, String.format("New latest edition %d is not greater than current latest edition %d!", latestEdition, this.latestEdition));
 +                      return;
 +              }
 +              this.latestEdition = latestEdition;
 +      }
 +
 +      /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public long getTime() {
 +              return time;
 +      }
 +
 +      /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public Sone setTime(long time) {
 +              this.time = time;
 +              return this;
 +      }
 +
 +      /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public SoneStatus getStatus() {
 +              return status;
 +      }
 +
 +      /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public Sone setStatus(SoneStatus status) {
 +              Validation.begin().isNotNull("Sone Status", status).check();
 +              this.status = status;
 +              return this;
 +      }
 +
 +      /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public Profile getProfile() {
 +              return new Profile(profile);
 +      }
 +
 +      /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public void setProfile(Profile profile) {
 +              this.profile = new Profile(profile);
 +      }
 +
 +      /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public Client getClient() {
 +              return client;
 +      }
 +
 +      /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public Sone setClient(Client client) {
 +              this.client = client;
 +              return this;
 +      }
 +
 +      /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public boolean isKnown() {
 +              return known;
 +      }
 +
 +      /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public Sone setKnown(boolean known) {
 +              this.known = known;
 +              return this;
 +      }
 +
 +      /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public List<String> getFriends() {
 +              List<String> friends = new ArrayList<String>(friendSones);
 +              return friends;
 +      }
 +
 +      /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public boolean hasFriend(String friendSoneId) {
 +              return friendSones.contains(friendSoneId);
 +      }
 +
 +      /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public Sone addFriend(String friendSone) {
 +              if (!friendSone.equals(id)) {
 +                      friendSones.add(friendSone);
 +              }
 +              return this;
 +      }
 +
 +      /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public Sone removeFriend(String friendSoneId) {
 +              friendSones.remove(friendSoneId);
 +              return this;
 +      }
 +
 +      /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public List<Post> getPosts() {
 +              List<Post> sortedPosts;
 +              synchronized (this) {
 +                      sortedPosts = new ArrayList<Post>(posts);
 +              }
 +              Collections.sort(sortedPosts, Post.TIME_COMPARATOR);
 +              return sortedPosts;
 +      }
 +
 +      /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public Sone setPosts(Collection<Post> posts) {
 +              synchronized (this) {
 +                      this.posts.clear();
 +                      this.posts.addAll(posts);
 +              }
 +              return this;
 +      }
 +
 +      /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public void addPost(Post post) {
 +              if (post.getSone().equals(this) && posts.add(post)) {
 +                      logger.log(Level.FINEST, String.format("Adding %s to “%s”.", post, getName()));
 +              }
 +      }
 +
 +      /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public void removePost(Post post) {
 +              if (post.getSone().equals(this)) {
 +                      posts.remove(post);
 +              }
 +      }
 +
 +      /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public Set<PostReply> getReplies() {
 +              return Collections.unmodifiableSet(replies);
 +      }
 +
 +      /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public Sone setReplies(Collection<PostReply> replies) {
 +              this.replies.clear();
 +              this.replies.addAll(replies);
 +              return this;
 +      }
 +
 +      /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public void addReply(PostReply reply) {
 +              if (reply.getSone().equals(this)) {
 +                      replies.add(reply);
 +              }
 +      }
 +
 +      /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public void removeReply(PostReply reply) {
 +              if (reply.getSone().equals(this)) {
 +                      replies.remove(reply);
 +              }
 +      }
 +
 +      /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public Set<String> getLikedPostIds() {
 +              return Collections.unmodifiableSet(likedPostIds);
 +      }
 +
 +      /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public Sone setLikePostIds(Set<String> likedPostIds) {
 +              this.likedPostIds.clear();
 +              this.likedPostIds.addAll(likedPostIds);
 +              return this;
 +      }
 +
 +      /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public boolean isLikedPostId(String postId) {
 +              return likedPostIds.contains(postId);
 +      }
 +
 +      /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public Sone addLikedPostId(String postId) {
 +              likedPostIds.add(postId);
 +              return this;
 +      }
 +
 +      /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public Sone removeLikedPostId(String postId) {
 +              likedPostIds.remove(postId);
 +              return this;
 +      }
 +
 +      /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public Set<String> getLikedReplyIds() {
 +              return Collections.unmodifiableSet(likedReplyIds);
 +      }
 +
 +      /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public Sone setLikeReplyIds(Set<String> likedReplyIds) {
 +              this.likedReplyIds.clear();
 +              this.likedReplyIds.addAll(likedReplyIds);
 +              return this;
 +      }
 +
 +      /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public boolean isLikedReplyId(String replyId) {
 +              return likedReplyIds.contains(replyId);
 +      }
 +
 +      /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public Sone addLikedReplyId(String replyId) {
 +              likedReplyIds.add(replyId);
 +              return this;
 +      }
 +
 +      /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public Sone removeLikedReplyId(String replyId) {
 +              likedReplyIds.remove(replyId);
 +              return this;
 +      }
 +
 +      /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public List<Album> getAlbums() {
 +              return Collections.unmodifiableList(albums);
 +      }
 +
 +      /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public List<Album> getAllAlbums() {
 +              List<Album> flatAlbums = new ArrayList<Album>();
 +              flatAlbums.addAll(albums);
 +              int lastAlbumIndex = 0;
 +              while (lastAlbumIndex < flatAlbums.size()) {
 +                      int previousAlbumCount = flatAlbums.size();
 +                      for (Album album : new ArrayList<Album>(flatAlbums.subList(lastAlbumIndex, flatAlbums.size()))) {
 +                              flatAlbums.addAll(album.getAlbums());
 +                      }
 +                      lastAlbumIndex = previousAlbumCount;
 +              }
 +              return flatAlbums;
 +      }
 +
 +      /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public List<Image> getAllImages() {
 +              List<Image> allImages = new ArrayList<Image>();
 +              for (Album album : getAllAlbums()) {
 +                      allImages.addAll(album.getImages());
 +              }
 +              return allImages;
 +      }
 +
 +      /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public void addAlbum(Album album) {
 +              Validation.begin().isNotNull("Album", album).check().isEqual("Album Owner", album.getSone(), this).check();
++              if (!albums.contains(album)) {
++                      albums.add(album);
++              }
 +      }
 +
 +      /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public void setAlbums(Collection<? extends Album> albums) {
 +              Validation.begin().isNotNull("Albums", albums).check();
 +              this.albums.clear();
 +              for (Album album : albums) {
 +                      addAlbum(album);
 +              }
 +      }
 +
 +      /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public void removeAlbum(Album album) {
 +              Validation.begin().isNotNull("Album", album).check().isEqual("Album Owner", album.getSone(), this).check();
 +              albums.remove(album);
 +      }
 +
 +      /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public Album moveAlbumUp(Album album) {
 +              Validation.begin().isNotNull("Album", album).check().isEqual("Album Owner", album.getSone(), this).isNull("Album Parent", album.getParent()).check();
 +              int oldIndex = albums.indexOf(album);
 +              if (oldIndex <= 0) {
 +                      return null;
 +              }
 +              albums.remove(oldIndex);
 +              albums.add(oldIndex - 1, album);
 +              return albums.get(oldIndex);
 +      }
 +
 +      /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public Album moveAlbumDown(Album album) {
 +              Validation.begin().isNotNull("Album", album).check().isEqual("Album Owner", album.getSone(), this).isNull("Album Parent", album.getParent()).check();
 +              int oldIndex = albums.indexOf(album);
 +              if ((oldIndex < 0) || (oldIndex >= (albums.size() - 1))) {
 +                      return null;
 +              }
 +              albums.remove(oldIndex);
 +              albums.add(oldIndex + 1, album);
 +              return albums.get(oldIndex);
 +      }
 +
 +      /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public Options getOptions() {
 +              return options;
 +      }
 +
 +      //
 +      // FINGERPRINTABLE METHODS
 +      //
 +
 +      /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public synchronized String getFingerprint() {
 +              StringBuilder fingerprint = new StringBuilder();
 +              fingerprint.append(profile.getFingerprint());
 +
 +              fingerprint.append("Posts(");
 +              for (Post post : getPosts()) {
 +                      fingerprint.append("Post(").append(post.getId()).append(')');
 +              }
 +              fingerprint.append(")");
 +
 +              List<PostReply> replies = new ArrayList<PostReply>(getReplies());
 +              Collections.sort(replies, Reply.TIME_COMPARATOR);
 +              fingerprint.append("Replies(");
 +              for (PostReply reply : replies) {
 +                      fingerprint.append("Reply(").append(reply.getId()).append(')');
 +              }
 +              fingerprint.append(')');
 +
 +              List<String> likedPostIds = new ArrayList<String>(getLikedPostIds());
 +              Collections.sort(likedPostIds);
 +              fingerprint.append("LikedPosts(");
 +              for (String likedPostId : likedPostIds) {
 +                      fingerprint.append("Post(").append(likedPostId).append(')');
 +              }
 +              fingerprint.append(')');
 +
 +              List<String> likedReplyIds = new ArrayList<String>(getLikedReplyIds());
 +              Collections.sort(likedReplyIds);
 +              fingerprint.append("LikedReplies(");
 +              for (String likedReplyId : likedReplyIds) {
 +                      fingerprint.append("Reply(").append(likedReplyId).append(')');
 +              }
 +              fingerprint.append(')');
 +
 +              fingerprint.append("Albums(");
 +              for (Album album : albums) {
 +                      fingerprint.append(album.getFingerprint());
 +              }
 +              fingerprint.append(')');
 +
 +              return fingerprint.toString();
 +      }
 +
 +      //
 +      // INTERFACE Comparable<Sone>
 +      //
 +
 +      /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public int compareTo(Sone sone) {
 +              return NICE_NAME_COMPARATOR.compare(this, sone);
 +      }
 +
 +      //
 +      // OBJECT METHODS
 +      //
 +
 +      /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public int hashCode() {
 +              return id.hashCode();
 +      }
 +
 +      /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public boolean equals(Object object) {
 +              if (!(object instanceof Sone)) {
 +                      return false;
 +              }
 +              return ((Sone) object).getId().equals(id);
 +      }
 +
 +      /**
 +       * {@inheritDoc}
 +       */
 +      @Override
 +      public String toString() {
 +              return getClass().getName() + "[identity=" + identity + ",requestUri=" + requestUri + ",insertUri(" + String.valueOf(insertUri).length() + "),friends(" + friendSones.size() + "),posts(" + posts.size() + "),replies(" + replies.size() + ")]";
 +      }
 +
 +}
@@@ -24,8 -24,7 +24,9 @@@ import java.util.logging.Logger
  
  import net.pterodactylus.sone.core.Core;
  import net.pterodactylus.sone.core.FreenetInterface;
+ import net.pterodactylus.sone.core.WebOfTrustUpdater;
 +import net.pterodactylus.sone.database.Database;
 +import net.pterodactylus.sone.database.memory.MemoryDatabase;
  import net.pterodactylus.sone.fcp.FcpInterface;
  import net.pterodactylus.sone.freenet.PluginStoreConfigurationBackend;
  import net.pterodactylus.sone.freenet.plugin.PluginConnector;
@@@ -85,7 -84,7 +86,7 @@@ public class SonePlugin implements Fred
        }
  
        /** The version. */
-       public static final Version VERSION = new Version(0, 8, 1);
+       public static final Version VERSION = new Version(0, 8, 2);
  
        /** The logger. */
        private static final Logger logger = Logging.getLogger(SonePlugin.class);
                        identityManager = new IdentityManager(webOfTrustConnector);
                        identityManager.setContext("Sone");
  
 +                      /* create Sone database. */
 +                      Database soneDatabase = new MemoryDatabase();
 +
+                       /* create trust updater. */
+                       WebOfTrustUpdater trustUpdater = new WebOfTrustUpdater(webOfTrustConnector);
+                       trustUpdater.init();
                        /* create core. */
-                       core = new Core(oldConfiguration, soneDatabase, freenetInterface, identityManager);
 -                      core = new Core(oldConfiguration, freenetInterface, identityManager, trustUpdater);
++                      core = new Core(oldConfiguration, soneDatabase, freenetInterface, identityManager, trustUpdater);
  
                        /* create the web interface. */
                        webInterface = new WebInterface(this);
@@@ -21,6 -21,7 +21,7 @@@ import net.pterodactylus.sone.core.Core
  import net.pterodactylus.sone.data.Profile;
  import net.pterodactylus.sone.data.Sone;
  import net.pterodactylus.sone.data.Sone.ShowCustomAvatars;
+ import net.pterodactylus.sone.freenet.wot.OwnIdentity;
  import net.pterodactylus.sone.freenet.wot.Trust;
  import net.pterodactylus.util.template.Accessor;
  import net.pterodactylus.util.template.ReflectionAccessor;
@@@ -69,7 -70,7 +70,7 @@@ public class ProfileAccessor extends Re
                                return null;
                        }
                        Sone remoteSone = profile.getSone();
 -                      if (core.isLocalSone(remoteSone)) {
 +                      if (remoteSone.isLocal()) {
                                /* always show your own avatars. */
                                return avatarId;
                        }
@@@ -83,7 -84,7 +84,7 @@@
                        if (showCustomAvatars == ShowCustomAvatars.FOLLOWED) {
                                return currentSone.hasFriend(remoteSone.getId()) ? avatarId : null;
                        }
-                       Trust trust = core.getTrust(currentSone, remoteSone);
+                       Trust trust = remoteSone.getIdentity().getTrust((OwnIdentity) currentSone.getIdentity());
                        if (trust == null) {
                                return null;
                        }
@@@ -24,6 -24,7 +24,7 @@@ import net.pterodactylus.sone.core.Core
  import net.pterodactylus.sone.data.Profile;
  import net.pterodactylus.sone.data.Sone;
  import net.pterodactylus.sone.data.Sone.SoneStatus;
+ import net.pterodactylus.sone.freenet.wot.OwnIdentity;
  import net.pterodactylus.sone.freenet.wot.Trust;
  import net.pterodactylus.sone.web.WebInterface;
  import net.pterodactylus.sone.web.ajax.GetTimesAjaxPage;
@@@ -75,6 -76,8 +76,6 @@@ public class SoneAccessor extends Refle
                Sone sone = (Sone) object;
                if (member.equals("niceName")) {
                        return getNiceName(sone);
 -              } else if (member.equals("local")) {
 -                      return core.isLocalSone(sone);
                } else if (member.equals("friend")) {
                        Sone currentSone = (Sone) templateContext.get("currentSone");
                        return (currentSone != null) && currentSone.hasFriend(sone.getId());
                        if (currentSone == null) {
                                return null;
                        }
-                       Trust trust = core.getTrust(currentSone, sone);
+                       Trust trust = sone.getIdentity().getTrust((OwnIdentity) currentSone.getIdentity());
                        logger.log(Level.FINEST, String.format("Trust for %s by %s: %s", sone, currentSone, trust));
                        if (trust == null) {
                                return new Trust(null, null, null);
@@@ -30,7 -30,7 +30,8 @@@ import net.pterodactylus.sone.core.Post
  import net.pterodactylus.sone.core.SoneProvider;
  import net.pterodactylus.sone.data.Post;
  import net.pterodactylus.sone.data.Sone;
 +import net.pterodactylus.sone.database.memory.MemorySone;
+ import net.pterodactylus.util.io.Closer;
  import net.pterodactylus.util.logging.Logging;
  import freenet.keys.FreenetURI;
  
@@@ -110,187 -110,193 +111,193 @@@ public class SoneTextParser implements 
        public Iterable<Part> parse(SoneTextParserContext context, Reader source) throws IOException {
                PartContainer parts = new PartContainer();
                BufferedReader bufferedReader = (source instanceof BufferedReader) ? (BufferedReader) source : new BufferedReader(source);
-               String line;
-               boolean lastLineEmpty = true;
-               int emptyLines = 0;
-               while ((line = bufferedReader.readLine()) != null) {
-                       if (line.trim().length() == 0) {
-                               if (lastLineEmpty) {
+               try {
+                       String line;
+                       boolean lastLineEmpty = true;
+                       int emptyLines = 0;
+                       while ((line = bufferedReader.readLine()) != null) {
+                               if (line.trim().length() == 0) {
+                                       if (lastLineEmpty) {
+                                               continue;
+                                       }
+                                       parts.add(new PlainTextPart("\n"));
+                                       ++emptyLines;
+                                       lastLineEmpty = emptyLines == 2;
                                        continue;
                                }
-                               parts.add(new PlainTextPart("\n"));
-                               ++emptyLines;
-                               lastLineEmpty = emptyLines == 2;
-                               continue;
-                       }
-                       emptyLines = 0;
-                       /*
-                        * lineComplete tracks whether the block you are parsing is the
-                        * first block of the line. this is important because sometimes you
-                        * have to add an additional line break.
-                        */
-                       boolean lineComplete = true;
-                       while (line.length() > 0) {
-                               int nextKsk = line.indexOf("KSK@");
-                               int nextChk = line.indexOf("CHK@");
-                               int nextSsk = line.indexOf("SSK@");
-                               int nextUsk = line.indexOf("USK@");
-                               int nextHttp = line.indexOf("http://");
-                               int nextHttps = line.indexOf("https://");
-                               int nextSone = line.indexOf("sone://");
-                               int nextPost = line.indexOf("post://");
-                               if ((nextKsk == -1) && (nextChk == -1) && (nextSsk == -1) && (nextUsk == -1) && (nextHttp == -1) && (nextHttps == -1) && (nextSone == -1) && (nextPost == -1)) {
-                                       if (lineComplete && !lastLineEmpty) {
-                                               parts.add(new PlainTextPart("\n" + line));
-                                       } else {
-                                               parts.add(new PlainTextPart(line));
+                               emptyLines = 0;
+                               /*
+                                * lineComplete tracks whether the block you are parsing is the
+                                * first block of the line. this is important because sometimes
+                                * you have to add an additional line break.
+                                */
+                               boolean lineComplete = true;
+                               while (line.length() > 0) {
+                                       int nextKsk = line.indexOf("KSK@");
+                                       int nextChk = line.indexOf("CHK@");
+                                       int nextSsk = line.indexOf("SSK@");
+                                       int nextUsk = line.indexOf("USK@");
+                                       int nextHttp = line.indexOf("http://");
+                                       int nextHttps = line.indexOf("https://");
+                                       int nextSone = line.indexOf("sone://");
+                                       int nextPost = line.indexOf("post://");
+                                       if ((nextKsk == -1) && (nextChk == -1) && (nextSsk == -1) && (nextUsk == -1) && (nextHttp == -1) && (nextHttps == -1) && (nextSone == -1) && (nextPost == -1)) {
+                                               if (lineComplete && !lastLineEmpty) {
+                                                       parts.add(new PlainTextPart("\n" + line));
+                                               } else {
+                                                       parts.add(new PlainTextPart(line));
+                                               }
+                                               break;
+                                       }
+                                       int next = Integer.MAX_VALUE;
+                                       LinkType linkType = null;
+                                       if ((nextKsk > -1) && (nextKsk < next)) {
+                                               next = nextKsk;
+                                               linkType = LinkType.KSK;
+                                       }
+                                       if ((nextChk > -1) && (nextChk < next)) {
+                                               next = nextChk;
+                                               linkType = LinkType.CHK;
+                                       }
+                                       if ((nextSsk > -1) && (nextSsk < next)) {
+                                               next = nextSsk;
+                                               linkType = LinkType.SSK;
+                                       }
+                                       if ((nextUsk > -1) && (nextUsk < next)) {
+                                               next = nextUsk;
+                                               linkType = LinkType.USK;
+                                       }
+                                       if ((nextHttp > -1) && (nextHttp < next)) {
+                                               next = nextHttp;
+                                               linkType = LinkType.HTTP;
+                                       }
+                                       if ((nextHttps > -1) && (nextHttps < next)) {
+                                               next = nextHttps;
+                                               linkType = LinkType.HTTPS;
+                                       }
+                                       if ((nextSone > -1) && (nextSone < next)) {
+                                               next = nextSone;
+                                               linkType = LinkType.SONE;
+                                       }
+                                       if ((nextPost > -1) && (nextPost < next)) {
+                                               next = nextPost;
+                                               linkType = LinkType.POST;
                                        }
-                                       break;
-                               }
-                               int next = Integer.MAX_VALUE;
-                               LinkType linkType = null;
-                               if ((nextKsk > -1) && (nextKsk < next)) {
-                                       next = nextKsk;
-                                       linkType = LinkType.KSK;
-                               }
-                               if ((nextChk > -1) && (nextChk < next)) {
-                                       next = nextChk;
-                                       linkType = LinkType.CHK;
-                               }
-                               if ((nextSsk > -1) && (nextSsk < next)) {
-                                       next = nextSsk;
-                                       linkType = LinkType.SSK;
-                               }
-                               if ((nextUsk > -1) && (nextUsk < next)) {
-                                       next = nextUsk;
-                                       linkType = LinkType.USK;
-                               }
-                               if ((nextHttp > -1) && (nextHttp < next)) {
-                                       next = nextHttp;
-                                       linkType = LinkType.HTTP;
-                               }
-                               if ((nextHttps > -1) && (nextHttps < next)) {
-                                       next = nextHttps;
-                                       linkType = LinkType.HTTPS;
-                               }
-                               if ((nextSone > -1) && (nextSone < next)) {
-                                       next = nextSone;
-                                       linkType = LinkType.SONE;
-                               }
-                               if ((nextPost > -1) && (nextPost < next)) {
-                                       next = nextPost;
-                                       linkType = LinkType.POST;
-                               }
  
-                               /* cut off “freenet:” from before keys. */
-                               if (((linkType == LinkType.KSK) || (linkType == LinkType.CHK) || (linkType == LinkType.SSK) || (linkType == LinkType.USK)) && (next >= 8) && (line.substring(next - 8, next).equals("freenet:"))) {
-                                       next -= 8;
-                                       line = line.substring(0, next) + line.substring(next + 8);
-                               }
+                                       /* cut off “freenet:” from before keys. */
+                                       if (((linkType == LinkType.KSK) || (linkType == LinkType.CHK) || (linkType == LinkType.SSK) || (linkType == LinkType.USK)) && (next >= 8) && (line.substring(next - 8, next).equals("freenet:"))) {
+                                               next -= 8;
+                                               line = line.substring(0, next) + line.substring(next + 8);
+                                       }
  
-                               /* if there is text before the next item, write it out. */
-                               if (lineComplete && !lastLineEmpty) {
-                                       parts.add(new PlainTextPart("\n"));
-                               }
-                               if (next > 0) {
-                                       parts.add(new PlainTextPart(line.substring(0, next)));
-                                       line = line.substring(next);
-                                       next = 0;
-                               }
-                               lineComplete = false;
+                                       /* if there is text before the next item, write it out. */
+                                       if (lineComplete && !lastLineEmpty) {
+                                               parts.add(new PlainTextPart("\n"));
+                                       }
+                                       if (next > 0) {
+                                               parts.add(new PlainTextPart(line.substring(0, next)));
+                                               line = line.substring(next);
+                                               next = 0;
+                                       }
+                                       lineComplete = false;
  
-                               if (linkType == LinkType.SONE) {
-                                       if (line.length() >= (7 + 43)) {
-                                               String soneId = line.substring(7, 50);
-                                               Sone sone = soneProvider.getSone(soneId, false);
-                                               if (sone == null) {
-                                                       /*
-                                                        * don’t use create=true above, we don’t want the
-                                                        * empty shell.
-                                                        */
-                                                       sone = new MemorySone(soneId, false);
+                                       if (linkType == LinkType.SONE) {
+                                               if (line.length() >= (7 + 43)) {
+                                                       String soneId = line.substring(7, 50);
+                                                       Sone sone = soneProvider.getSone(soneId, false);
+                                                       if (sone == null) {
+                                                               /*
+                                                                * don’t use create=true above, we don’t want
+                                                                * the empty shell.
+                                                                */
 -                                                              sone = new Sone(soneId);
++                                                              sone = new MemorySone(soneId, false);
+                                                       }
+                                                       parts.add(new SonePart(sone));
+                                                       line = line.substring(50);
+                                               } else {
+                                                       parts.add(new PlainTextPart(line));
+                                                       line = "";
                                                }
-                                               parts.add(new SonePart(sone));
-                                               line = line.substring(50);
-                                       } else {
-                                               parts.add(new PlainTextPart(line));
-                                               line = "";
+                                               continue;
                                        }
-                                       continue;
-                               }
-                               if (linkType == LinkType.POST) {
-                                       if (line.length() >= (7 + 36)) {
-                                               String postId = line.substring(7, 43);
-                                               Post post = postProvider.getPost(postId, false);
-                                               if ((post != null) && (post.getSone() != null)) {
-                                                       parts.add(new PostPart(post));
+                                       if (linkType == LinkType.POST) {
+                                               if (line.length() >= (7 + 36)) {
+                                                       String postId = line.substring(7, 43);
+                                                       Post post = postProvider.getPost(postId, false);
+                                                       if ((post != null) && (post.getSone() != null)) {
+                                                               parts.add(new PostPart(post));
+                                                       } else {
+                                                               parts.add(new PlainTextPart(line.substring(0, 43)));
+                                                       }
+                                                       line = line.substring(43);
                                                } else {
-                                                       parts.add(new PlainTextPart(line.substring(0, 43)));
+                                                       parts.add(new PlainTextPart(line));
+                                                       line = "";
                                                }
-                                               line = line.substring(43);
-                                       } else {
-                                               parts.add(new PlainTextPart(line));
-                                               line = "";
+                                               continue;
                                        }
-                                       continue;
-                               }
-                               Matcher matcher = whitespacePattern.matcher(line);
-                               int nextSpace = matcher.find(0) ? matcher.start() : line.length();
-                               String link = line.substring(0, nextSpace);
-                               String name = link;
-                               logger.log(Level.FINER, String.format("Found link: %s", link));
-                               logger.log(Level.FINEST, String.format("CHK: %d, SSK: %d, USK: %d", nextChk, nextSsk, nextUsk));
+                                       Matcher matcher = whitespacePattern.matcher(line);
+                                       int nextSpace = matcher.find(0) ? matcher.start() : line.length();
+                                       String link = line.substring(0, nextSpace);
+                                       String name = link;
+                                       logger.log(Level.FINER, String.format("Found link: %s", link));
+                                       logger.log(Level.FINEST, String.format("CHK: %d, SSK: %d, USK: %d", nextChk, nextSsk, nextUsk));
  
-                               if ((linkType == LinkType.KSK) || (linkType == LinkType.CHK) || (linkType == LinkType.SSK) || (linkType == LinkType.USK)) {
-                                       FreenetURI uri;
-                                       if (name.indexOf('?') > -1) {
-                                               name = name.substring(0, name.indexOf('?'));
-                                       }
-                                       if (name.endsWith("/")) {
-                                               name = name.substring(0, name.length() - 1);
-                                       }
-                                       try {
-                                               uri = new FreenetURI(name);
-                                               name = uri.lastMetaString();
-                                               if (name == null) {
-                                                       name = uri.getDocName();
+                                       if ((linkType == LinkType.KSK) || (linkType == LinkType.CHK) || (linkType == LinkType.SSK) || (linkType == LinkType.USK)) {
+                                               FreenetURI uri;
+                                               if (name.indexOf('?') > -1) {
+                                                       name = name.substring(0, name.indexOf('?'));
                                                }
-                                               if (name == null) {
-                                                       name = link.substring(0, Math.min(9, link.length()));
+                                               if (name.endsWith("/")) {
+                                                       name = name.substring(0, name.length() - 1);
                                                }
-                                               boolean fromPostingSone = ((linkType == LinkType.SSK) || (linkType == LinkType.USK)) && (context != null) && (context.getPostingSone() != null) && link.substring(4, Math.min(link.length(), 47)).equals(context.getPostingSone().getId());
-                                               parts.add(new FreenetLinkPart(link, name, fromPostingSone));
-                                       } catch (MalformedURLException mue1) {
-                                               /* not a valid link, insert as plain text. */
-                                               parts.add(new PlainTextPart(link));
-                                       } catch (NullPointerException npe1) {
-                                               /* FreenetURI sometimes throws these, too. */
-                                               parts.add(new PlainTextPart(link));
-                                       } catch (ArrayIndexOutOfBoundsException aioobe1) {
-                                               /* oh, and these, too. */
-                                               parts.add(new PlainTextPart(link));
-                                       }
-                               } else if ((linkType == LinkType.HTTP) || (linkType == LinkType.HTTPS)) {
-                                       name = link.substring(linkType == LinkType.HTTP ? 7 : 8);
-                                       int firstSlash = name.indexOf('/');
-                                       int lastSlash = name.lastIndexOf('/');
-                                       if ((lastSlash - firstSlash) > 3) {
-                                               name = name.substring(0, firstSlash + 1) + "…" + name.substring(lastSlash);
-                                       }
-                                       if (name.endsWith("/")) {
-                                               name = name.substring(0, name.length() - 1);
-                                       }
-                                       if (((name.indexOf('/') > -1) && (name.indexOf('.') < name.lastIndexOf('.', name.indexOf('/'))) || ((name.indexOf('/') == -1) && (name.indexOf('.') < name.lastIndexOf('.')))) && name.startsWith("www.")) {
-                                               name = name.substring(4);
-                                       }
-                                       if (name.indexOf('?') > -1) {
-                                               name = name.substring(0, name.indexOf('?'));
+                                               try {
+                                                       uri = new FreenetURI(name);
+                                                       name = uri.lastMetaString();
+                                                       if (name == null) {
+                                                               name = uri.getDocName();
+                                                       }
+                                                       if (name == null) {
+                                                               name = link.substring(0, Math.min(9, link.length()));
+                                                       }
+                                                       boolean fromPostingSone = ((linkType == LinkType.SSK) || (linkType == LinkType.USK)) && (context != null) && (context.getPostingSone() != null) && link.substring(4, Math.min(link.length(), 47)).equals(context.getPostingSone().getId());
+                                                       parts.add(new FreenetLinkPart(link, name, fromPostingSone));
+                                               } catch (MalformedURLException mue1) {
+                                                       /* not a valid link, insert as plain text. */
+                                                       parts.add(new PlainTextPart(link));
+                                               } catch (NullPointerException npe1) {
+                                                       /* FreenetURI sometimes throws these, too. */
+                                                       parts.add(new PlainTextPart(link));
+                                               } catch (ArrayIndexOutOfBoundsException aioobe1) {
+                                                       /* oh, and these, too. */
+                                                       parts.add(new PlainTextPart(link));
+                                               }
+                                       } else if ((linkType == LinkType.HTTP) || (linkType == LinkType.HTTPS)) {
+                                               name = link.substring(linkType == LinkType.HTTP ? 7 : 8);
+                                               int firstSlash = name.indexOf('/');
+                                               int lastSlash = name.lastIndexOf('/');
+                                               if ((lastSlash - firstSlash) > 3) {
+                                                       name = name.substring(0, firstSlash + 1) + "…" + name.substring(lastSlash);
+                                               }
+                                               if (name.endsWith("/")) {
+                                                       name = name.substring(0, name.length() - 1);
+                                               }
+                                               if (((name.indexOf('/') > -1) && (name.indexOf('.') < name.lastIndexOf('.', name.indexOf('/'))) || ((name.indexOf('/') == -1) && (name.indexOf('.') < name.lastIndexOf('.')))) && name.startsWith("www.")) {
+                                                       name = name.substring(4);
+                                               }
+                                               if (name.indexOf('?') > -1) {
+                                                       name = name.substring(0, name.indexOf('?'));
+                                               }
+                                               parts.add(new LinkPart(link, name));
                                        }
-                                       parts.add(new LinkPart(link, name));
+                                       line = line.substring(nextSpace);
                                }
-                               line = line.substring(nextSpace);
+                               lastLineEmpty = false;
+                       }
+               } finally {
+                       if (bufferedReader != source) {
+                               Closer.close(bufferedReader);
                        }
-                       lastLineEmpty = false;
                }
                for (int partIndex = parts.size() - 1; partIndex >= 0; --partIndex) {
                        Part part = parts.getPart(partIndex);
@@@ -111,7 -111,7 +111,7 @@@ public class SearchPage extends SoneTem
                        throw new RedirectException("index.html");
                }
  
 -              Set<Sone> sones = webInterface.getCore().getSones();
 +              Collection<Sone> sones = webInterface.getCore().getSones();
                Set<Hit<Sone>> soneHits = getHits(sones, phrases, SoneStringGenerator.COMPLETE_GENERATOR);
  
                Set<Hit<Post>> postHits;
         *            The string generator for the objects
         * @return The hits for the given phrases
         */
-       private <T> Set<Hit<T>> getHits(Collection<T> objects, List<Phrase> phrases, StringGenerator<T> stringGenerator) {
+       private static <T> Set<Hit<T>> getHits(Collection<T> objects, List<Phrase> phrases, StringGenerator<T> stringGenerator) {
                Set<Hit<T>> hits = new HashSet<Hit<T>>();
                for (T object : objects) {
                        String objectString = stringGenerator.generateString(object);
         *            The query to parse
         * @return The parsed phrases
         */
-       private List<Phrase> parseSearchPhrases(String query) {
+       private static List<Phrase> parseSearchPhrases(String query) {
                List<String> parsedPhrases = null;
                try {
                        parsedPhrases = StringEscaper.parseLine(query);
         *            The expression to search
         * @return The score of the expression
         */
-       private double calculateScore(List<Phrase> phrases, String expression) {
+       private static double calculateScore(List<Phrase> phrases, String expression) {
                logger.log(Level.FINEST, String.format("Calculating Score for “%s”…", expression));
                double optionalHits = 0;
                double requiredHits = 0;
@@@ -122,6 -122,7 +122,7 @@@ import net.pterodactylus.util.template.
  import net.pterodactylus.util.template.HtmlFilter;
  import net.pterodactylus.util.template.MatchFilter;
  import net.pterodactylus.util.template.ModFilter;
+ import net.pterodactylus.util.template.PaginationFilter;
  import net.pterodactylus.util.template.Provider;
  import net.pterodactylus.util.template.ReflectionAccessor;
  import net.pterodactylus.util.template.ReplaceFilter;
@@@ -259,6 -260,7 +260,7 @@@ public class WebInterface implements Co
                templateContextFactory.addFilter("in", new ContainsFilter());
                templateContextFactory.addFilter("unique", new UniqueElementFilter());
                templateContextFactory.addFilter("mod", new ModFilter());
+               templateContextFactory.addFilter("paginate", new PaginationFilter());
                templateContextFactory.addProvider(Provider.TEMPLATE_CONTEXT_PROVIDER);
                templateContextFactory.addProvider(new ClassPathTemplateProvider());
                templateContextFactory.addTemplateObject("webInterface", this);
         *         currently logged in
         */
        public Sone getCurrentSone(ToadletContext toadletContext, boolean create) {
 -              Set<Sone> localSones = getCore().getLocalSones();
 +              Collection<Sone> localSones = getCore().getLocalSones();
                if (localSones.size() == 1) {
                        return localSones.iterator().next();
                }
        }
  
        /**
 -       * Returns all {@link Core#isLocalSone(Sone) local Sone}s that are
 -       * referenced by {@link SonePart}s in the given text (after parsing it using
 +       * Returns all {@link Core#getLocalSones() local Sone}s that are referenced
 +       * by {@link SonePart}s in the given text (after parsing it using
         * {@link SoneTextParser}).
         *
         * @param text
         */
        @Override
        public void newPostFound(Post post) {
 -              boolean isLocal = getCore().isLocalSone(post.getSone());
 -              if (isLocal) {
 +              if (post.getSone().isLocal()) {
                        localPostNotification.add(post);
                } else {
                        newPostNotification.add(post);
                }
                if (!hasFirstStartNotification()) {
 -                      notificationManager.addNotification(isLocal ? localPostNotification : newPostNotification);
 -                      if (!getMentionedSones(post.getText()).isEmpty() && !isLocal) {
 +                      notificationManager.addNotification(post.getSone().isLocal() ? localPostNotification : newPostNotification);
 +                      if (!getMentionedSones(post.getText()).isEmpty() && !post.getSone().isLocal()) {
                                mentionNotification.add(post);
                                notificationManager.addNotification(mentionNotification);
                        }
         */
        @Override
        public void newReplyFound(PostReply reply) {
 -              boolean isLocal = getCore().isLocalSone(reply.getSone());
 -              if (isLocal) {
 +              if (reply.getSone().isLocal()) {
                        localReplyNotification.add(reply);
                } else {
                        newReplyNotification.add(reply);
                }
                if (!hasFirstStartNotification()) {
 -                      notificationManager.addNotification(isLocal ? localReplyNotification : newReplyNotification);
 -                      if (!getMentionedSones(reply.getText()).isEmpty() && !isLocal && (reply.getPost().getSone() != null) && (reply.getTime() <= System.currentTimeMillis())) {
 +                      notificationManager.addNotification(reply.getSone().isLocal() ? localReplyNotification : newReplyNotification);
 +                      if (!getMentionedSones(reply.getText()).isEmpty() && !reply.getSone().isLocal() && (reply.getPost().getSone() != null) && (reply.getTime() <= System.currentTimeMillis())) {
                                mentionNotification.add(reply.getPost());
                                notificationManager.addNotification(mentionNotification);
                        }
@@@ -63,7 -63,7 +63,7 @@@ public class EditImageAjaxPage extends 
                if (image == null) {
                        return createErrorJsonObject("invalid-image-id");
                }
 -              if (!webInterface.getCore().isLocalSone(image.getSone())) {
 +              if (!image.getSone().isLocal()) {
                        return createErrorJsonObject("not-authorized");
                }
                if ("true".equals(request.getHttpRequest().getParam("moveLeft"))) {
@@@ -80,7 -80,7 +80,7 @@@
                String description = request.getHttpRequest().getParam("description").trim();
                image.setTitle(title).setDescription(TextFilter.filter(request.getHttpRequest().getHeader("host"), description));
                webInterface.getCore().touchConfiguration();
-               return createSuccessJsonObject().put("imageId", image.getId()).put("title", image.getTitle()).put("description", image.getDescription()).put("parsedDescription", (String) parserFilter.format(new TemplateContext(), image.getDescription(), new MapBuilder<String, String>().put("sone", image.getSone().getId()).get()));
+               return createSuccessJsonObject().put("imageId", image.getId()).put("title", image.getTitle()).put("description", image.getDescription()).put("parsedDescription", (String) parserFilter.format(new TemplateContext(), image.getDescription(), new MapBuilder<String, Object>().put("sone", image.getSone()).get()));
        }
  
  }
@@@ -63,7 -63,7 +63,7 @@@ public class GetLikesAjaxPage extends J
                        return createErrorJsonObject("invalid-" + type + "-id");
                }
                if ("post".equals(type)) {
 -                      Post post = webInterface.getCore().getPost(id);
 +                      Post post = webInterface.getCore().getPost(id, false);
                        Set<Sone> sones = webInterface.getCore().getLikes(post);
                        return createSuccessJsonObject().put("likes", sones.size()).put("sones", getSones(sones));
                } else if ("reply".equals(type)) {
@@@ -94,7 -94,7 +94,7 @@@
         *            The Sones to convert to an array
         * @return The Sones, sorted by name
         */
-       private JsonArray getSones(Set<Sone> sones) {
+       private static JsonArray getSones(Set<Sone> sones) {
                JsonArray soneArray = new JsonArray();
                List<Sone> sortedSones = new ArrayList<Sone>(sones);
                Collections.sort(sortedSones, Sone.NICE_NAME_COMPARATOR);