Merge branch 'next' into new-database-38 new-database-38
authorDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Sat, 12 Jan 2013 16:41:27 +0000 (17:41 +0100)
committerDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Mon, 14 Jan 2013 05:32:50 +0000 (06:32 +0100)
Conflicts:
src/main/java/net/pterodactylus/sone/core/Core.java
src/main/java/net/pterodactylus/sone/web/SearchPage.java

1  2 
src/main/java/net/pterodactylus/sone/core/Core.java
src/main/java/net/pterodactylus/sone/main/SonePlugin.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/test/java/net/pterodactylus/sone/text/SoneTextParserTest.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;
@@@ -48,7 -47,6 +48,7 @@@ import net.pterodactylus.sone.data.Sone
  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;
@@@ -61,6 -59,7 +61,7 @@@ import net.pterodactylus.util.config.Co
  import net.pterodactylus.util.logging.Logging;
  import net.pterodactylus.util.number.Numbers;
  import net.pterodactylus.util.service.AbstractService;
+ import net.pterodactylus.util.thread.NamedThreadFactory;
  import net.pterodactylus.util.thread.Ticker;
  import net.pterodactylus.util.validation.EqualityValidator;
  import net.pterodactylus.util.validation.IntegerRangeValidator;
@@@ -79,6 -78,9 +80,9 @@@ public class Core extends AbstractServi
        /** The logger. */
        private static final Logger logger = Logging.getLogger(Core.class);
  
+       /** The start time. */
+       private final long startupTime = System.currentTimeMillis();
        /** The options. */
        private final Options options = new Options();
  
@@@ -94,9 -96,6 +98,9 @@@
        /** Whether we’re currently saving the configuration. */
        private boolean storingConfiguration = false;
  
 +      /** The database. */
 +      private Database database;
 +
        /** The identity manager. */
        private final IdentityManager identityManager;
  
        private final ImageInserter imageInserter;
  
        /** Sone downloader thread-pool. */
-       private final ExecutorService soneDownloaders = Executors.newFixedThreadPool(10);
+       private final ExecutorService soneDownloaders = Executors.newFixedThreadPool(10, new NamedThreadFactory("Sone Downloader %2$d"));
  
        /** The update checker. */
        private final UpdateChecker updateChecker;
        /* 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
         * @param webOfTrustUpdater
         *            The WebOfTrust updater
         */
 -      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);
        //
  
        /**
+        * Returns the time Sone was started.
+        *
+        * @return The startup time (in milliseconds since Jan 1, 1970 UTC)
+        */
+       public long getStartupTime() {
+               return startupTime;
+       }
+       /**
         * Sets the configuration to use. This will automatically save the current
         * configuration to the given configuration.
         *
         * @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
         * @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()) {
                        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;
        }
  
        /**
                        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;
        }
  
        /**
        }
  
        /**
 -       * 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
                                                if (!storedPosts.contains(post)) {
                                                        if (post.getTime() < getSoneFollowingTime(sone)) {
                                                                knownPosts.add(post.getId());
+                                                               post.setKnown(true);
                                                        } else if (!knownPosts.contains(post.getId())) {
-                                                               sone.setKnown(false);
                                                                coreListenerManager.fireNewPostFound(post);
                                                        }
                                                }
                                                if (!storedReplies.contains(reply)) {
                                                        if (reply.getTime() < getSoneFollowingTime(sone)) {
                                                                knownReplies.add(reply.getId());
+                                                               reply.setKnown(true);
                                                        } else if (!knownReplies.contains(reply.getId())) {
-                                                               reply.setKnown(false);
                                                                coreListenerManager.fireNewReplyFound(reply);
                                                        }
                                                }
                        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;
                }
 +              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 {
         *            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)) {
+               Validation.begin().isNotNull("Text", text).check().isGreater("Text Length", text.length(), 0).check();
 +              if (!sone.isLocal()) {
                        logger.log(Level.FINE, String.format("Tried to create post for non-local Sone: %s", sone));
                        return null;
                }
         *            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)) {
+               Validation.begin().isNotNull("Text", text).check().isGreater("Text Length", text.trim().length(), 0).check();
 +              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;
                }
         * @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) {
         */
        @Override
        public void serviceStop() {
 -              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();
         *            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;
                }
         */
        @Override
        public void identityUpdated(OwnIdentity ownIdentity, final Identity identity) {
-               new Thread(new Runnable() {
+               soneDownloaders.execute(new Runnable() {
  
                        @Override
                        @SuppressWarnings("synthetic-access")
                                soneDownloader.addSone(sone);
                                soneDownloader.fetchSone(sone);
                        }
-               }).start();
+               });
        }
  
        /**
                                }
                        }
                }
 -              synchronized (remoteSones) {
 -                      remoteSones.remove(identity.getId());
 -              }
 +              database.removeSone(identity.getId());
                coreListenerManager.fireSoneRemoved(sone);
        }
  
@@@ -25,8 -25,6 +25,8 @@@ 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;
@@@ -86,7 -84,7 +86,7 @@@ public class SonePlugin implements Fred
        }
  
        /** The version. */
-       public static final Version VERSION = new Version(0, 8, 2);
+       public static final Version VERSION = new Version(0, 8, 4);
  
        /** The logger. */
        private static final Logger logger = Logging.getLogger(SonePlugin.class);
                        /* create web of trust connector. */
                        PluginConnector pluginConnector = new PluginConnector(pluginRespirator);
                        webOfTrustConnector = new WebOfTrustConnector(pluginConnector);
-                       identityManager = new IdentityManager(webOfTrustConnector);
-                       identityManager.setContext("Sone");
+                       identityManager = new IdentityManager(webOfTrustConnector, "Sone");
  
 +                      /* create Sone database. */
 +                      Database soneDatabase = new MemoryDatabase();
 +
                        /* create trust updater. */
                        WebOfTrustUpdater trustUpdater = new WebOfTrustUpdater(webOfTrustConnector);
                        trustUpdater.init();
  
                        /* create core. */
 -                      core = new Core(oldConfiguration, freenetInterface, identityManager, trustUpdater);
 +                      core = new Core(oldConfiguration, soneDatabase, freenetInterface, identityManager, trustUpdater);
  
                        /* create the web interface. */
                        webInterface = new WebInterface(this);
@@@ -30,7 -30,6 +30,7 @@@ 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;
@@@ -56,28 -55,50 +56,50 @@@ public class SoneTextParser implements 
        private enum LinkType {
  
                /** Link is a KSK. */
-               KSK,
+               KSK("KSK@"),
  
                /** Link is a CHK. */
-               CHK,
+               CHK("CHK@"),
  
                /** Link is an SSK. */
-               SSK,
+               SSK("SSK@"),
  
                /** Link is a USK. */
-               USK,
+               USK("USK@"),
  
                /** Link is HTTP. */
-               HTTP,
+               HTTP("http://"),
  
                /** Link is HTTPS. */
-               HTTPS,
+               HTTPS("https://"),
  
                /** Link is a Sone. */
-               SONE,
+               SONE("sone://"),
  
                /** Link is a post. */
-               POST,
+               POST("post://");
+               /** The scheme identifying this link type. */
+               private final String scheme;
+               /**
+                * Creates a new link type identified by the given scheme.
+                *
+                * @param scheme
+                *            The scheme of the link type
+                */
+               private LinkType(String scheme) {
+                       this.scheme = scheme;
+               }
+               /**
+                * Returns the scheme of this link type.
+                *
+                * @return The scheme of this link type
+                */
+               public String getScheme() {
+                       return scheme;
+               }
  
        }
  
                                        }
                                        lineComplete = false;
  
+                                       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 there is no text after the scheme, it’s not a link! */
+                                       if (link.equals(linkType.getScheme())) {
+                                               parts.add(new PlainTextPart(linkType.getScheme()));
+                                               line = line.substring(linkType.getScheme().length());
+                                               continue;
+                                       }
                                        if (linkType == LinkType.SONE) {
                                                if (line.length() >= (7 + 43)) {
                                                        String soneId = line.substring(7, 50);
                                                                 * 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);
                                                }
                                                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));
  
                                        if ((linkType == LinkType.KSK) || (linkType == LinkType.CHK) || (linkType == LinkType.SSK) || (linkType == LinkType.USK)) {
                                                FreenetURI uri;
@@@ -111,7 -111,27 +111,27 @@@ public class SearchPage extends SoneTem
                        throw new RedirectException("index.html");
                }
  
 -              Set<Sone> sones = webInterface.getCore().getSones();
+               /* check for a couple of shortcuts. */
+               if (phrases.size() == 1) {
+                       String phrase = phrases.get(0).getPhrase();
+                       /* is it a Sone ID? */
+                       redirectIfNotNull(getSoneId(phrase), "viewSone.html?sone=");
+                       /* is it a post ID? */
+                       redirectIfNotNull(getPostId(phrase), "viewPost.html?post=");
+                       /* is it a reply ID? show the post. */
+                       redirectIfNotNull(getReplyPostId(phrase), "viewPost.html?post=");
+                       /* is it an album ID? */
+                       redirectIfNotNull(getAlbumId(phrase), "imageBrowser.html?album=");
+                       /* is it an image ID? */
+                       redirectIfNotNull(getImageId(phrase), "imageBrowser.html?image=");
+               }
 +              Collection<Sone> sones = webInterface.getCore().getSones();
                Set<Hit<Sone>> soneHits = getHits(sones, phrases, SoneStringGenerator.COMPLETE_GENERATOR);
  
                Set<Hit<Post>> postHits;
        }
  
        /**
+        * Throws a
+        * {@link net.pterodactylus.sone.web.page.FreenetTemplatePage.RedirectException}
+        * if the given object is not {@code null}, appending the object to the
+        * given target URL.
+        *
+        * @param object
+        *            The object on which to redirect
+        * @param target
+        *            The target of the redirect
+        * @throws RedirectException
+        *             if {@code object} is not {@code null}
+        */
+       private static void redirectIfNotNull(String object, String target) throws RedirectException {
+               if (object != null) {
+                       throw new RedirectException(target + object);
+               }
+       }
+       /**
+        * If the given phrase contains a Sone ID (optionally prefixed by
+        * “sone://”), returns said Sone ID, otherwise return {@code null}.
+        *
+        * @param phrase
+        *            The phrase that maybe is a Sone ID
+        * @return The Sone ID, or {@code null}
+        */
+       private String getSoneId(String phrase) {
+               String soneId = phrase.startsWith("sone://") ? phrase.substring(7) : phrase;
+               return (webInterface.getCore().getSone(soneId, false) != null) ? soneId : null;
+       }
+       /**
+        * If the given phrase contains a post ID (optionally prefixed by
+        * “post://”), returns said post ID, otherwise return {@code null}.
+        *
+        * @param phrase
+        *            The phrase that maybe is a post ID
+        * @return The post ID, or {@code null}
+        */
+       private String getPostId(String phrase) {
+               String postId = phrase.startsWith("post://") ? phrase.substring(7) : phrase;
+               return (webInterface.getCore().getPost(postId, false) != null) ? postId : null;
+       }
+       /**
+        * If the given phrase contains a reply ID (optionally prefixed by
+        * “reply://”), returns the ID of the post the reply belongs to, otherwise
+        * return {@code null}.
+        *
+        * @param phrase
+        *            The phrase that maybe is a reply ID
+        * @return The reply’s post ID, or {@code null}
+        */
+       private String getReplyPostId(String phrase) {
+               String replyId = phrase.startsWith("reply://") ? phrase.substring(8) : phrase;
+               return (webInterface.getCore().getReply(replyId, false) != null) ? webInterface.getCore().getReply(replyId, false).getPost().getId() : null;
+       }
+       /**
+        * If the given phrase contains an album ID (optionally prefixed by
+        * “album://”), returns said album ID, otherwise return {@code null}.
+        *
+        * @param phrase
+        *            The phrase that maybe is an album ID
+        * @return The album ID, or {@code null}
+        */
+       private String getAlbumId(String phrase) {
+               String albumId = phrase.startsWith("album://") ? phrase.substring(8) : phrase;
+               return (webInterface.getCore().getAlbum(albumId, false) != null) ? albumId : null;
+       }
+       /**
+        * If the given phrase contains an image ID (optionally prefixed by
+        * “image://”), returns said image ID, otherwise return {@code null}.
+        *
+        * @param phrase
+        *            The phrase that maybe is an image ID
+        * @return The image ID, or {@code null}
+        */
+       private String getImageId(String phrase) {
+               String imageId = phrase.startsWith("image://") ? phrase.substring(8) : phrase;
+               return (webInterface.getCore().getImage(imageId, false) != null) ? imageId : null;
+       }
+       /**
         * Converts a given object into a {@link String}.
         *
         * @param <T>
@@@ -103,18 -103,13 +103,13 @@@ import net.pterodactylus.sone.web.ajax.
  import net.pterodactylus.sone.web.page.FreenetRequest;
  import net.pterodactylus.sone.web.page.PageToadlet;
  import net.pterodactylus.sone.web.page.PageToadletFactory;
- import net.pterodactylus.util.cache.Cache;
- import net.pterodactylus.util.cache.CacheException;
- import net.pterodactylus.util.cache.CacheItem;
- import net.pterodactylus.util.cache.DefaultCacheItem;
- import net.pterodactylus.util.cache.MemoryCache;
- import net.pterodactylus.util.cache.ValueRetriever;
  import net.pterodactylus.util.collection.SetBuilder;
  import net.pterodactylus.util.collection.filter.Filters;
  import net.pterodactylus.util.logging.Logging;
  import net.pterodactylus.util.notify.Notification;
  import net.pterodactylus.util.notify.NotificationManager;
  import net.pterodactylus.util.notify.TemplateNotification;
+ import net.pterodactylus.util.template.ClassPathTemplateProvider;
  import net.pterodactylus.util.template.CollectionSortFilter;
  import net.pterodactylus.util.template.ContainsFilter;
  import net.pterodactylus.util.template.DateFilter;
@@@ -123,15 -118,13 +118,13 @@@ import net.pterodactylus.util.template.
  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;
  import net.pterodactylus.util.template.StoreFilter;
  import net.pterodactylus.util.template.Template;
- import net.pterodactylus.util.template.TemplateContext;
  import net.pterodactylus.util.template.TemplateContextFactory;
- import net.pterodactylus.util.template.TemplateException;
  import net.pterodactylus.util.template.TemplateParser;
+ import net.pterodactylus.util.template.TemplateProvider;
  import net.pterodactylus.util.template.XmlFilter;
  import net.pterodactylus.util.thread.Ticker;
  import net.pterodactylus.util.version.Version;
@@@ -222,7 -215,6 +215,6 @@@ public class WebInterface implements Co
         * @param sonePlugin
         *            The Sone plugin
         */
-       @SuppressWarnings("synthetic-access")
        public WebInterface(SonePlugin sonePlugin) {
                this.sonePlugin = sonePlugin;
                formPassword = sonePlugin.pluginRespirator().getToadletContainer().getFormPassword();
                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.addProvider(TemplateProvider.TEMPLATE_CONTEXT_PROVIDER);
+               templateContextFactory.addProvider(new ClassPathTemplateProvider(WebInterface.class, "/templates/"));
                templateContextFactory.addTemplateObject("webInterface", this);
                templateContextFactory.addTemplateObject("formPassword", formPassword);
  
         *         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);
                        }
                notificationManager.addNotification(imageInsertFailedNotification);
        }
  
-       /**
-        * Template provider implementation that uses
-        * {@link WebInterface#createReader(String)} to load templates for
-        * inclusion.
-        *
-        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
-        */
-       private class ClassPathTemplateProvider implements Provider {
-               /** Cache for templates. */
-               private final Cache<String, Template> templateCache = new MemoryCache<String, Template>(new ValueRetriever<String, Template>() {
-                       @Override
-                       @SuppressWarnings("synthetic-access")
-                       public CacheItem<Template> retrieve(String key) throws CacheException {
-                               Template template = findTemplate(key);
-                               if (template != null) {
-                                       return new DefaultCacheItem<Template>(template);
-                               }
-                               return null;
-                       }
-               });
-               /**
-                * {@inheritDoc}
-                */
-               @Override
-               @SuppressWarnings("synthetic-access")
-               public Template getTemplate(TemplateContext templateContext, String templateName) {
-                       try {
-                               return templateCache.get(templateName);
-                       } catch (CacheException ce1) {
-                               logger.log(Level.WARNING, String.format("Could not get template for %s!", templateName), ce1);
-                               return null;
-                       }
-               }
-               /**
-                * Locates a template in the class path.
-                *
-                * @param templateName
-                *            The name of the template to load
-                * @return The loaded template, or {@code null} if no template could be
-                *         found
-                */
-               @SuppressWarnings("synthetic-access")
-               private Template findTemplate(String templateName) {
-                       Reader templateReader = createReader("/templates/" + templateName);
-                       if (templateReader == null) {
-                               return null;
-                       }
-                       Template template = null;
-                       try {
-                               template = TemplateParser.parse(templateReader);
-                       } catch (TemplateException te1) {
-                               logger.log(Level.WARNING, String.format("Could not parse template “%s” for inclusion!", templateName), te1);
-                       }
-                       return template;
-               }
-       }
  }
@@@ -19,11 -19,11 +19,12 @@@ package net.pterodactylus.sone.text
  
  import java.io.IOException;
  import java.io.StringReader;
+ import java.util.Arrays;
  
  import junit.framework.TestCase;
  import net.pterodactylus.sone.core.SoneProvider;
  import net.pterodactylus.sone.data.Sone;
 +import net.pterodactylus.sone.database.memory.MemorySone;
  
  /**
   * JUnit test case for {@link SoneTextParser}.
@@@ -42,6 -42,7 +43,7 @@@ public class SoneTextParserTest extend
         * @throws IOException
         *             if an I/O error occurs
         */
+       @SuppressWarnings("static-method")
        public void testPlainText() throws IOException {
                SoneTextParser soneTextParser = new SoneTextParser(null, null);
                Iterable<Part> parts;
@@@ -68,6 -69,7 +70,7 @@@
         * @throws IOException
         *             if an I/O error occurs
         */
+       @SuppressWarnings("static-method")
        public void testKSKLinks() throws IOException {
                SoneTextParser soneTextParser = new SoneTextParser(null, null);
                Iterable<Part> parts;
@@@ -94,7 -96,7 +97,7 @@@
         * @throws IOException
         *             if an I/O error occurs
         */
-       @SuppressWarnings("synthetic-access")
+       @SuppressWarnings({ "synthetic-access", "static-method" })
        public void testEmptyLinesAndSoneLinks() throws IOException {
                SoneTextParser soneTextParser = new SoneTextParser(new TestSoneProvider(), null);
                Iterable<Part> parts;
                assertEquals("Part Text", "Some text.\n\nLink to [Sone|DAxKQzS48mtaQc7sUVHIgx3fnWZPQBz0EueBreUVWrU] and stuff.", convertText(parts, PlainTextPart.class, SonePart.class));
        }
  
+       /**
+        * Test for a bug discovered in Sone 0.8.4 where a plain “http://” would be
+        * parsed into a link.
+        *
+        * @throws IOException
+        *             if an I/O error occurs
+        */
+       @SuppressWarnings({ "synthetic-access", "static-method" })
+       public void testEmpyHttpLinks() throws IOException {
+               SoneTextParser soneTextParser = new SoneTextParser(new TestSoneProvider(), null);
+               Iterable<Part> parts;
+               /* check empty http links. */
+               parts = soneTextParser.parse(null, new StringReader("Some text. Empty link: http:// – nice!"));
+               assertNotNull("Parts", parts);
+               assertEquals("Part Text", "Some text. Empty link: http:// – nice!", convertText(parts, PlainTextPart.class));
+       }
        //
        // PRIVATE METHODS
        //
         *            valid
         * @return The converted text
         */
-       private String convertText(Iterable<Part> parts, Class<?>... validClasses) {
+       private static String convertText(Iterable<Part> parts, Class<?>... validClasses) {
                StringBuilder text = new StringBuilder();
                for (Part part : parts) {
                        assertNotNull("Part", part);
                                }
                        }
                        if (!classValid) {
-                               assertEquals("Part’s Class", null, part.getClass());
+                               fail("Part’s Class (" + part.getClass() + ") is not one of " + Arrays.toString(validClasses));
                        }
                        if (part instanceof PlainTextPart) {
                                text.append(((PlainTextPart) part).getText());
                 */
                @Override
                public Sone getSone(final String soneId, boolean create) {
 -                      return new Sone(soneId) {
 +                      return new MemorySone(soneId, false) {
  
                                /**
                                 * {@inheritDoc}