Merge branch 'next' into dev/image
authorDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Tue, 20 Sep 2011 20:03:35 +0000 (22:03 +0200)
committerDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Tue, 20 Sep 2011 20:03:35 +0000 (22:03 +0200)
174 files changed:
pom.xml
src/main/java/net/pterodactylus/sone/core/Core.java
src/main/java/net/pterodactylus/sone/core/CoreListener.java
src/main/java/net/pterodactylus/sone/core/CoreListenerManager.java
src/main/java/net/pterodactylus/sone/core/FreenetInterface.java
src/main/java/net/pterodactylus/sone/core/Options.java
src/main/java/net/pterodactylus/sone/core/PostProvider.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/core/SoneDownloader.java
src/main/java/net/pterodactylus/sone/core/SoneException.java
src/main/java/net/pterodactylus/sone/core/SoneInsertListener.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/core/SoneInsertListenerManager.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/core/SoneInserter.java
src/main/java/net/pterodactylus/sone/core/SoneProvider.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/core/SoneRescuer.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/core/UpdateChecker.java
src/main/java/net/pterodactylus/sone/data/Post.java
src/main/java/net/pterodactylus/sone/data/Profile.java
src/main/java/net/pterodactylus/sone/data/Sone.java
src/main/java/net/pterodactylus/sone/fcp/AbstractSoneCommand.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/fcp/CreatePostCommand.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/fcp/CreateReplyCommand.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/fcp/DeletePostCommand.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/fcp/DeleteReplyCommand.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/fcp/FcpInterface.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/fcp/GetLocalSonesCommand.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/fcp/GetPostCommand.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/fcp/GetPostFeedCommand.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/fcp/GetPostsCommand.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/fcp/GetSoneCommand.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/fcp/GetSonesCommand.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/fcp/LikePostCommand.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/fcp/LikeReplyCommand.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/fcp/VersionCommand.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/freenet/L10nFilter.java
src/main/java/net/pterodactylus/sone/freenet/PluginStoreConfigurationBackend.java
src/main/java/net/pterodactylus/sone/freenet/SimpleFieldSetBuilder.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/freenet/StringBucket.java
src/main/java/net/pterodactylus/sone/freenet/fcp/AbstractCommand.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/freenet/fcp/Command.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/freenet/fcp/FcpException.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/freenet/plugin/ConnectorListener.java
src/main/java/net/pterodactylus/sone/freenet/wot/DefaultIdentity.java
src/main/java/net/pterodactylus/sone/freenet/wot/IdentityManager.java
src/main/java/net/pterodactylus/sone/freenet/wot/WebOfTrustConnector.java
src/main/java/net/pterodactylus/sone/main/SonePlugin.java
src/main/java/net/pterodactylus/sone/notify/ListNotification.java
src/main/java/net/pterodactylus/sone/notify/ListNotificationFilters.java
src/main/java/net/pterodactylus/sone/template/GetPagePlugin.java
src/main/java/net/pterodactylus/sone/template/NotificationManagerAccessor.java [deleted file]
src/main/java/net/pterodactylus/sone/template/ParserFilter.java
src/main/java/net/pterodactylus/sone/template/RequestChangeFilter.java
src/main/java/net/pterodactylus/sone/template/SoneAccessor.java
src/main/java/net/pterodactylus/sone/template/UniqueElementFilter.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/text/FreenetLinkParser.java [deleted file]
src/main/java/net/pterodactylus/sone/text/FreenetLinkParserContext.java [deleted file]
src/main/java/net/pterodactylus/sone/text/FreenetLinkPart.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/text/LinkPart.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/text/Parser.java
src/main/java/net/pterodactylus/sone/text/ParserContext.java
src/main/java/net/pterodactylus/sone/text/Part.java
src/main/java/net/pterodactylus/sone/text/PartContainer.java
src/main/java/net/pterodactylus/sone/text/PlainTextPart.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/text/PostPart.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/text/SonePart.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/text/SoneTextParser.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/text/SoneTextParserContext.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/text/TemplatePart.java [deleted file]
src/main/java/net/pterodactylus/sone/text/TextFilter.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/AboutPage.java
src/main/java/net/pterodactylus/sone/web/BookmarkPage.java
src/main/java/net/pterodactylus/sone/web/BookmarksPage.java
src/main/java/net/pterodactylus/sone/web/CreateAlbumPage.java
src/main/java/net/pterodactylus/sone/web/CreatePostPage.java
src/main/java/net/pterodactylus/sone/web/CreateReplyPage.java
src/main/java/net/pterodactylus/sone/web/CreateSonePage.java
src/main/java/net/pterodactylus/sone/web/DeleteAlbumPage.java
src/main/java/net/pterodactylus/sone/web/DeleteImagePage.java
src/main/java/net/pterodactylus/sone/web/DeletePostPage.java
src/main/java/net/pterodactylus/sone/web/DeleteProfileFieldPage.java
src/main/java/net/pterodactylus/sone/web/DeleteReplyPage.java
src/main/java/net/pterodactylus/sone/web/DeleteSonePage.java
src/main/java/net/pterodactylus/sone/web/DismissNotificationPage.java
src/main/java/net/pterodactylus/sone/web/DistrustPage.java
src/main/java/net/pterodactylus/sone/web/EditAlbumPage.java
src/main/java/net/pterodactylus/sone/web/EditImagePage.java
src/main/java/net/pterodactylus/sone/web/EditProfileFieldPage.java
src/main/java/net/pterodactylus/sone/web/EditProfilePage.java
src/main/java/net/pterodactylus/sone/web/FollowSonePage.java
src/main/java/net/pterodactylus/sone/web/GetImagePage.java
src/main/java/net/pterodactylus/sone/web/ImageBrowserPage.java
src/main/java/net/pterodactylus/sone/web/IndexPage.java
src/main/java/net/pterodactylus/sone/web/KnownSonesPage.java
src/main/java/net/pterodactylus/sone/web/LikePage.java
src/main/java/net/pterodactylus/sone/web/LockSonePage.java
src/main/java/net/pterodactylus/sone/web/LoginPage.java
src/main/java/net/pterodactylus/sone/web/LogoutPage.java
src/main/java/net/pterodactylus/sone/web/MarkAsKnownPage.java
src/main/java/net/pterodactylus/sone/web/OptionsPage.java
src/main/java/net/pterodactylus/sone/web/RescuePage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/SearchPage.java
src/main/java/net/pterodactylus/sone/web/SoneTemplatePage.java
src/main/java/net/pterodactylus/sone/web/TrustPage.java
src/main/java/net/pterodactylus/sone/web/UnbookmarkPage.java
src/main/java/net/pterodactylus/sone/web/UnfollowSonePage.java
src/main/java/net/pterodactylus/sone/web/UnlikePage.java
src/main/java/net/pterodactylus/sone/web/UnlockSonePage.java
src/main/java/net/pterodactylus/sone/web/UntrustPage.java
src/main/java/net/pterodactylus/sone/web/UploadImagePage.java
src/main/java/net/pterodactylus/sone/web/ViewPostPage.java
src/main/java/net/pterodactylus/sone/web/ViewSonePage.java
src/main/java/net/pterodactylus/sone/web/WebInterface.java
src/main/java/net/pterodactylus/sone/web/ajax/BookmarkAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/CreatePostAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/CreateReplyAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/DeletePostAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/DeleteProfileFieldAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/DeleteReplyAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/DismissNotificationAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/DistrustAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/EditProfileFieldAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/FollowSoneAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/GetLikesAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/GetNotificationAjaxPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/ajax/GetPostAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/GetReplyAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/GetStatusAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/GetTimesAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/GetTranslationPage.java
src/main/java/net/pterodactylus/sone/web/ajax/JsonPage.java
src/main/java/net/pterodactylus/sone/web/ajax/LikeAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/LockSoneAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/MarkAsKnownAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/MoveProfileFieldAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/TrustAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/UnbookmarkAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/UnfollowSoneAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/UnlikeAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/UnlockSoneAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/UntrustAjaxPage.java
src/main/java/net/pterodactylus/sone/web/page/FreenetRequest.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/page/FreenetTemplatePage.java
src/main/java/net/pterodactylus/sone/web/page/Page.java [deleted file]
src/main/java/net/pterodactylus/sone/web/page/PageToadlet.java
src/main/java/net/pterodactylus/sone/web/page/PageToadletFactory.java
src/main/java/net/pterodactylus/sone/web/page/RedirectPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/page/StaticPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/page/TemplatePage.java [deleted file]
src/main/resources/i18n/sone.en.properties
src/main/resources/static/css/sone.css
src/main/resources/static/images/sone-offline.png [new file with mode: 0644]
src/main/resources/static/javascript/jquery.fieldselection.js [new file with mode: 0644]
src/main/resources/static/javascript/sone.js
src/main/resources/templates/include/head.html
src/main/resources/templates/include/pagination.html
src/main/resources/templates/include/soneMenu.html [new file with mode: 0644]
src/main/resources/templates/include/updateStatus.html
src/main/resources/templates/include/viewPost.html
src/main/resources/templates/include/viewReply.html
src/main/resources/templates/include/viewSone.html
src/main/resources/templates/index.html
src/main/resources/templates/insert/index.html
src/main/resources/templates/knownSones.html
src/main/resources/templates/notify/mentionNotification.html [new file with mode: 0644]
src/main/resources/templates/notify/newPostNotification.html
src/main/resources/templates/notify/newReplyNotification.html
src/main/resources/templates/notify/newSoneNotification.html
src/main/resources/templates/notify/rescuingSonesNotification.html [deleted file]
src/main/resources/templates/notify/soneInsertNotification.html [new file with mode: 0644]
src/main/resources/templates/notify/sonesRescuedNotification.html [deleted file]
src/main/resources/templates/options.html
src/main/resources/templates/rescue.html [new file with mode: 0644]
src/main/resources/templates/viewSone.html
src/test/java/net/pterodactylus/sone/text/FreenetLinkParserTest.java [deleted file]
src/test/java/net/pterodactylus/sone/text/SoneTextParserTest.java [new file with mode: 0644]

diff --git a/pom.xml b/pom.xml
index a849a98..e59d521 100644 (file)
--- a/pom.xml
+++ b/pom.xml
@@ -2,12 +2,12 @@
        <modelVersion>4.0.0</modelVersion>
        <groupId>net.pterodactylus</groupId>
        <artifactId>sone</artifactId>
-       <version>0.6.1</version>
+       <version>0.6.7</version>
        <dependencies>
                <dependency>
                        <groupId>net.pterodactylus</groupId>
                        <artifactId>utils</artifactId>
-                       <version>0.9.4-SNAPSHOT</version>
+                       <version>0.10.1-SNAPSHOT</version>
                </dependency>
                <dependency>
                        <groupId>junit</groupId>
index 343fe69..e67bf68 100644 (file)
@@ -25,6 +25,9 @@ import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.Map.Entry;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
@@ -36,10 +39,12 @@ import net.pterodactylus.sone.data.Client;
 import net.pterodactylus.sone.data.Image;
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Profile;
-import net.pterodactylus.sone.data.Profile.Field;
 import net.pterodactylus.sone.data.Reply;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.data.TemporaryImage;
+import net.pterodactylus.sone.data.Profile.Field;
+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;
@@ -51,6 +56,11 @@ import net.pterodactylus.util.config.Configuration;
 import net.pterodactylus.util.config.ConfigurationException;
 import net.pterodactylus.util.logging.Logging;
 import net.pterodactylus.util.number.Numbers;
+import net.pterodactylus.util.service.AbstractService;
+import net.pterodactylus.util.thread.Ticker;
+import net.pterodactylus.util.validation.EqualityValidator;
+import net.pterodactylus.util.validation.IntegerRangeValidator;
+import net.pterodactylus.util.validation.OrValidator;
 import net.pterodactylus.util.validation.Validation;
 import net.pterodactylus.util.version.Version;
 import freenet.keys.FreenetURI;
@@ -60,7 +70,7 @@ import freenet.keys.FreenetURI;
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
-public class Core implements IdentityListener, UpdateListener, ImageInsertListener {
+public class Core extends AbstractService implements IdentityListener, UpdateListener, SoneProvider, PostProvider, SoneInsertListener, ImageInsertListener {
 
        /**
         * Enumeration for the possible states of a {@link Sone}.
@@ -112,11 +122,14 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen
        /** The image inserter. */
        private final ImageInserter imageInserter;
 
+       /** Sone downloader thread-pool. */
+       private final ExecutorService soneDownloaders = Executors.newFixedThreadPool(10);
+
        /** The update checker. */
        private final UpdateChecker updateChecker;
 
-       /** Whether the core has been stopped. */
-       private volatile boolean stopped;
+       /** The FCP interface. */
+       private volatile FcpInterface fcpInterface;
 
        /** The Sones’ statuses. */
        /* synchronize access on itself. */
@@ -130,6 +143,10 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen
        /* synchronize access on this on localSones. */
        private final Map<Sone, SoneInserter> soneInserters = new HashMap<Sone, SoneInserter>();
 
+       /** Sone rescuers. */
+       /* 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 Map<String, Sone> localSones = new HashMap<String, Sone>();
@@ -180,6 +197,12 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen
        /** All temporary images. */
        private Map<String, TemporaryImage> temporaryImages = new HashMap<String, TemporaryImage>();
 
+       /** Ticker for threads that mark own elements as known. */
+       private Ticker localElementTicker = new Ticker();
+
+       /** The time the configuration was last touched. */
+       private volatile long lastConfigurationUpdate;
+
        /**
         * Creates a new core.
         *
@@ -191,6 +214,7 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen
         *            The identity manager
         */
        public Core(Configuration configuration, FreenetInterface freenetInterface, IdentityManager identityManager) {
+               super("Sone Core");
                this.configuration = configuration;
                this.freenetInterface = freenetInterface;
                this.identityManager = identityManager;
@@ -236,7 +260,7 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen
         */
        public void setConfiguration(Configuration configuration) {
                this.configuration = configuration;
-               saveConfiguration();
+               touchConfiguration();
        }
 
        /**
@@ -267,6 +291,16 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen
        }
 
        /**
+        * Sets the FCP interface to use.
+        *
+        * @param fcpInterface
+        *            The FCP interface to use
+        */
+       public void setFcpInterface(FcpInterface fcpInterface) {
+               this.fcpInterface = fcpInterface;
+       }
+
+       /**
         * Returns the status of the given Sone.
         *
         * @param sone
@@ -294,6 +328,26 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen
        }
 
        /**
+        * Returns the Sone rescuer for the given local Sone.
+        *
+        * @param sone
+        *            The local Sone to get the rescuer for
+        * @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) {
+                       SoneRescuer soneRescuer = soneRescuers.get(sone);
+                       if (soneRescuer == null) {
+                               soneRescuer = new SoneRescuer(this, soneDownloader, sone);
+                               soneRescuers.put(sone, soneRescuer);
+                               soneRescuer.start();
+                       }
+                       return soneRescuer;
+               }
+       }
+
+       /**
         * Returns whether the given Sone is currently locked.
         *
         * @param sone
@@ -344,6 +398,7 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen
         * @return The Sone with the given ID, or {@code null} if there is no such
         *         Sone
         */
+       @Override
        public Sone getSone(String id, boolean create) {
                if (isLocalSone(id)) {
                        return getLocalSone(id);
@@ -566,6 +621,7 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen
         *            exists, {@code false} to return {@code null}
         * @return The post, or {@code null} if there is no such post
         */
+       @Override
        public Post getPost(String postId, boolean create) {
                synchronized (posts) {
                        Post post = posts.get(postId);
@@ -656,6 +712,7 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen
         */
        public List<Reply> getReplies(Post post) {
                Set<Sone> sones = getSones();
+               @SuppressWarnings("hiding")
                List<Reply> replies = new ArrayList<Reply>();
                for (Sone sone : sones) {
                        for (Reply reply : sone.getReplies()) {
@@ -748,6 +805,7 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen
         * @return All bookmarked posts
         */
        public Set<Post> getBookmarkedPosts() {
+               @SuppressWarnings("hiding")
                Set<Post> posts = new HashSet<Post>();
                synchronized (bookmarkedPosts) {
                        for (String bookmarkedPostId : bookmarkedPosts) {
@@ -926,36 +984,11 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen
                        /* 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);
                        setSoneStatus(sone, SoneStatus.idle);
                        loadSone(sone);
-                       if (!preferences.isSoneRescueMode()) {
-                               soneInserter.start();
-                       }
-                       new Thread(new Runnable() {
-
-                               @Override
-                               @SuppressWarnings("synthetic-access")
-                               public void run() {
-                                       if (!preferences.isSoneRescueMode()) {
-                                               return;
-                                       }
-                                       logger.log(Level.INFO, "Trying to restore Sone from Freenet…");
-                                       coreListenerManager.fireRescuingSone(sone);
-                                       lockSone(sone);
-                                       long edition = sone.getLatestEdition();
-                                       while (!stopped && (edition >= 0) && preferences.isSoneRescueMode()) {
-                                               logger.log(Level.FINE, "Downloading edition " + edition + "…");
-                                               soneDownloader.fetchSone(sone, sone.getRequestUri().setKeyType("SSK").setDocName("Sone-" + edition));
-                                               --edition;
-                                       }
-                                       logger.log(Level.INFO, "Finished restoring Sone from Freenet, starting Inserter…");
-                                       saveSone(sone);
-                                       coreListenerManager.fireRescuedSone(sone);
-                                       soneInserter.start();
-                               }
-
-                       }, "Sone Downloader").start();
+                       soneInserter.start();
                        return sone;
                }
        }
@@ -976,7 +1009,8 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen
                }
                Sone sone = addLocalSone(ownIdentity);
                sone.getOptions().addBooleanOption("AutoFollow", new DefaultOption<Boolean>(false));
-               saveSone(sone);
+               sone.addFriend("nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI");
+               touchConfiguration();
                return sone;
        }
 
@@ -1009,6 +1043,7 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen
                                        for (Sone localSone : getLocalSones()) {
                                                if (localSone.getOptions().getBooleanOption("AutoFollow").get()) {
                                                        localSone.addFriend(sone.getId());
+                                                       touchConfiguration();
                                                }
                                        }
                                }
@@ -1016,15 +1051,15 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen
                        remoteSones.put(identity.getId(), sone);
                        soneDownloader.addSone(sone);
                        setSoneStatus(sone, SoneStatus.unknown);
-                       new Thread(new Runnable() {
+                       soneDownloaders.execute(new Runnable() {
 
                                @Override
                                @SuppressWarnings("synthetic-access")
                                public void run() {
-                                       soneDownloader.fetchSone(sone);
+                                       soneDownloader.fetchSone(sone, sone.getRequestUri());
                                }
 
-                       }, "Sone Downloader").start();
+                       });
                        return sone;
                }
        }
@@ -1121,14 +1156,28 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen
        }
 
        /**
-        * Updates the stores Sone with the given Sone.
+        * Updates the stored Sone with the given Sone.
         *
         * @param sone
         *            The updated Sone
         */
        public void updateSone(Sone sone) {
+               updateSone(sone, false);
+       }
+
+       /**
+        * Updates the stored Sone with the given Sone. If {@code soneRescueMode} is
+        * {@code true}, an older Sone than the current Sone can be given to restore
+        * an old state.
+        *
+        * @param sone
+        *            The Sone to update
+        * @param soneRescueMode
+        *            {@code true} if the stored Sone should be updated regardless
+        *            of the age of the given Sone
+        */
+       public void updateSone(Sone sone, boolean soneRescueMode) {
                if (hasSone(sone.getId())) {
-                       boolean soneRescueMode = isLocalSone(sone) && preferences.isSoneRescueMode();
                        Sone storedSone = getSone(sone.getId());
                        if (!soneRescueMode && !(sone.getTime() > storedSone.getTime())) {
                                logger.log(Level.FINE, "Downloaded Sone %s is not newer than stored Sone %s.", new Object[] { sone, storedSone });
@@ -1226,7 +1275,9 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen
                                return;
                        }
                        localSones.remove(sone.getId());
-                       soneInserters.remove(sone).stop();
+                       SoneInserter soneInserter = soneInserters.remove(sone);
+                       soneInserter.removeSoneInsertListener(this);
+                       soneInserter.stop();
                }
                try {
                        ((OwnIdentity) sone.getIdentity()).removeContext("Sone");
@@ -1253,7 +1304,7 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen
                        if (newSones.remove(sone.getId())) {
                                knownSones.add(sone.getId());
                                coreListenerManager.fireMarkSoneKnown(sone);
-                               saveConfiguration();
+                               touchConfiguration();
                        }
                }
        }
@@ -1271,6 +1322,10 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen
                        return;
                }
 
+               /* initialize options. */
+               sone.getOptions().addBooleanOption("AutoFollow", new DefaultOption<Boolean>(false));
+               sone.getOptions().addBooleanOption("EnableSoneInsertNotifications", new DefaultOption<Boolean>(false));
+
                /* load Sone. */
                String sonePrefix = "Sone/" + sone.getId();
                Long soneTime = configuration.getLongValue(sonePrefix + "/Time").getValue(null);
@@ -1301,6 +1356,7 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen
                }
 
                /* load posts. */
+               @SuppressWarnings("hiding")
                Set<Post> posts = new HashSet<Post>();
                while (true) {
                        String postPrefix = sonePrefix + "/Posts/" + posts.size();
@@ -1323,6 +1379,7 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen
                }
 
                /* load replies. */
+               @SuppressWarnings("hiding")
                Set<Reply> replies = new HashSet<Reply>();
                while (true) {
                        String replyPrefix = sonePrefix + "/Replies/" + replies.size();
@@ -1429,8 +1486,8 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen
                }
 
                /* load options. */
-               sone.getOptions().addBooleanOption("AutoFollow", new DefaultOption<Boolean>(false));
                sone.getOptions().getBooleanOption("AutoFollow").set(configuration.getBooleanValue(sonePrefix + "/Options/AutoFollow").getValue(null));
+               sone.getOptions().getBooleanOption("EnableSoneInsertNotifications").set(configuration.getBooleanValue(sonePrefix + "/Options/EnableSoneInsertNotifications").getValue(null));
 
                /* if we’re still here, Sone was loaded successfully. */
                synchronized (sone) {
@@ -1462,138 +1519,6 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen
        }
 
        /**
-        * Saves the given Sone. This will persist all local settings for the given
-        * Sone, such as the friends list and similar, private options.
-        *
-        * @param sone
-        *            The Sone to save
-        */
-       public synchronized void saveSone(Sone sone) {
-               if (!isLocalSone(sone)) {
-                       logger.log(Level.FINE, "Tried to save non-local Sone: %s", sone);
-                       return;
-               }
-               if (!(sone.getIdentity() instanceof OwnIdentity)) {
-                       logger.log(Level.WARNING, "Local Sone without OwnIdentity found, refusing to save: %s", sone);
-                       return;
-               }
-
-               logger.log(Level.INFO, "Saving Sone: %s", sone);
-               try {
-                       ((OwnIdentity) sone.getIdentity()).setProperty("Sone.LatestEdition", String.valueOf(sone.getLatestEdition()));
-
-                       /* save Sone into configuration. */
-                       String sonePrefix = "Sone/" + sone.getId();
-                       configuration.getLongValue(sonePrefix + "/Time").setValue(sone.getTime());
-                       configuration.getStringValue(sonePrefix + "/LastInsertFingerprint").setValue(soneInserters.get(sone).getLastInsertFingerprint());
-
-                       /* save profile. */
-                       Profile profile = sone.getProfile();
-                       configuration.getStringValue(sonePrefix + "/Profile/FirstName").setValue(profile.getFirstName());
-                       configuration.getStringValue(sonePrefix + "/Profile/MiddleName").setValue(profile.getMiddleName());
-                       configuration.getStringValue(sonePrefix + "/Profile/LastName").setValue(profile.getLastName());
-                       configuration.getIntValue(sonePrefix + "/Profile/BirthDay").setValue(profile.getBirthDay());
-                       configuration.getIntValue(sonePrefix + "/Profile/BirthMonth").setValue(profile.getBirthMonth());
-                       configuration.getIntValue(sonePrefix + "/Profile/BirthYear").setValue(profile.getBirthYear());
-
-                       /* save profile fields. */
-                       int fieldCounter = 0;
-                       for (Field profileField : profile.getFields()) {
-                               String fieldPrefix = sonePrefix + "/Profile/Fields/" + fieldCounter++;
-                               configuration.getStringValue(fieldPrefix + "/Name").setValue(profileField.getName());
-                               configuration.getStringValue(fieldPrefix + "/Value").setValue(profileField.getValue());
-                       }
-                       configuration.getStringValue(sonePrefix + "/Profile/Fields/" + fieldCounter + "/Name").setValue(null);
-
-                       /* save posts. */
-                       int postCounter = 0;
-                       for (Post post : sone.getPosts()) {
-                               String postPrefix = sonePrefix + "/Posts/" + postCounter++;
-                               configuration.getStringValue(postPrefix + "/ID").setValue(post.getId());
-                               configuration.getStringValue(postPrefix + "/Recipient").setValue((post.getRecipient() != null) ? post.getRecipient().getId() : null);
-                               configuration.getLongValue(postPrefix + "/Time").setValue(post.getTime());
-                               configuration.getStringValue(postPrefix + "/Text").setValue(post.getText());
-                       }
-                       configuration.getStringValue(sonePrefix + "/Posts/" + postCounter + "/ID").setValue(null);
-
-                       /* save replies. */
-                       int replyCounter = 0;
-                       for (Reply reply : sone.getReplies()) {
-                               String replyPrefix = sonePrefix + "/Replies/" + replyCounter++;
-                               configuration.getStringValue(replyPrefix + "/ID").setValue(reply.getId());
-                               configuration.getStringValue(replyPrefix + "/Post/ID").setValue(reply.getPost().getId());
-                               configuration.getLongValue(replyPrefix + "/Time").setValue(reply.getTime());
-                               configuration.getStringValue(replyPrefix + "/Text").setValue(reply.getText());
-                       }
-                       configuration.getStringValue(sonePrefix + "/Replies/" + replyCounter + "/ID").setValue(null);
-
-                       /* save post likes. */
-                       int postLikeCounter = 0;
-                       for (String postId : sone.getLikedPostIds()) {
-                               configuration.getStringValue(sonePrefix + "/Likes/Post/" + postLikeCounter++ + "/ID").setValue(postId);
-                       }
-                       configuration.getStringValue(sonePrefix + "/Likes/Post/" + postLikeCounter + "/ID").setValue(null);
-
-                       /* save reply likes. */
-                       int replyLikeCounter = 0;
-                       for (String replyId : sone.getLikedReplyIds()) {
-                               configuration.getStringValue(sonePrefix + "/Likes/Reply/" + replyLikeCounter++ + "/ID").setValue(replyId);
-                       }
-                       configuration.getStringValue(sonePrefix + "/Likes/Reply/" + replyLikeCounter + "/ID").setValue(null);
-
-                       /* save friends. */
-                       int friendCounter = 0;
-                       for (String friendId : sone.getFriends()) {
-                               configuration.getStringValue(sonePrefix + "/Friends/" + friendCounter++ + "/ID").setValue(friendId);
-                       }
-                       configuration.getStringValue(sonePrefix + "/Friends/" + friendCounter + "/ID").setValue(null);
-
-                       /* save albums. first, collect in a flat structure, top-level first. */
-                       List<Album> albums = Sone.flattenAlbums(sone.getAlbums());
-
-                       int albumCounter = 0;
-                       for (Album album : albums) {
-                               String albumPrefix = sonePrefix + "/Albums/" + albumCounter++;
-                               configuration.getStringValue(albumPrefix + "/ID").setValue(album.getId());
-                               configuration.getStringValue(albumPrefix + "/Title").setValue(album.getTitle());
-                               configuration.getStringValue(albumPrefix + "/Description").setValue(album.getDescription());
-                               configuration.getStringValue(albumPrefix + "/Parent").setValue(album.getParent() == null ? null : album.getParent().getId());
-                       }
-                       configuration.getStringValue(sonePrefix + "/Albums/" + albumCounter + "/ID").setValue(null);
-
-                       /* save images. */
-                       int imageCounter = 0;
-                       for (Album album : albums) {
-                               for (Image image : album.getImages()) {
-                                       if (!image.isInserted()) {
-                                               continue;
-                                       }
-                                       String imagePrefix = sonePrefix + "/Images/" + imageCounter++;
-                                       configuration.getStringValue(imagePrefix + "/ID").setValue(image.getId());
-                                       configuration.getStringValue(imagePrefix + "/Album").setValue(album.getId());
-                                       configuration.getStringValue(imagePrefix + "/Key").setValue(image.getKey());
-                                       configuration.getStringValue(imagePrefix + "/Title").setValue(image.getTitle());
-                                       configuration.getStringValue(imagePrefix + "/Description").setValue(image.getDescription());
-                                       configuration.getLongValue(imagePrefix + "/CreationTime").setValue(image.getCreationTime());
-                                       configuration.getIntValue(imagePrefix + "/Width").setValue(image.getWidth());
-                                       configuration.getIntValue(imagePrefix + "/Height").setValue(image.getHeight());
-                               }
-                       }
-                       configuration.getStringValue(sonePrefix + "/Images/" + imageCounter + "/ID").setValue(null);
-
-                       /* save options. */
-                       configuration.getBooleanValue(sonePrefix + "/Options/AutoFollow").setValue(sone.getOptions().getBooleanOption("AutoFollow").getReal());
-
-                       configuration.save();
-                       logger.log(Level.INFO, "Sone %s saved.", sone);
-               } catch (ConfigurationException ce1) {
-                       logger.log(Level.WARNING, "Could not save Sone: " + sone, ce1);
-               } catch (WebOfTrustException wote1) {
-                       logger.log(Level.WARNING, "Could not set WoT property for Sone: " + sone, wote1);
-               }
-       }
-
-       /**
         * Creates a new post.
         *
         * @param sone
@@ -1656,7 +1581,7 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen
                        logger.log(Level.FINE, "Tried to create post for non-local Sone: %s", sone);
                        return null;
                }
-               Post post = new Post(sone, time, text);
+               final Post post = new Post(sone, time, text);
                if (recipient != null) {
                        post.setRecipient(recipient);
                }
@@ -1668,7 +1593,17 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen
                        coreListenerManager.fireNewPostFound(post);
                }
                sone.addPost(post);
-               saveSone(sone);
+               touchConfiguration();
+               localElementTicker.registerEvent(System.currentTimeMillis() + 10 * 1000, new Runnable() {
+
+                       /**
+                        * {@inheritDoc}
+                        */
+                       @Override
+                       public void run() {
+                               markPostKnown(post);
+                       }
+               }, "Mark " + post + " read.");
                return post;
        }
 
@@ -1687,11 +1622,12 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen
                synchronized (posts) {
                        posts.remove(post.getId());
                }
+               coreListenerManager.firePostRemoved(post);
                synchronized (newPosts) {
                        markPostKnown(post);
                        knownPosts.remove(post.getId());
                }
-               saveSone(post.getSone());
+               touchConfiguration();
        }
 
        /**
@@ -1706,7 +1642,7 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen
                        if (newPosts.remove(post.getId())) {
                                knownPosts.add(post.getId());
                                coreListenerManager.fireMarkPostKnown(post);
-                               saveConfiguration();
+                               touchConfiguration();
                        }
                }
        }
@@ -1788,7 +1724,7 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen
                        logger.log(Level.FINE, "Tried to create reply for non-local Sone: %s", sone);
                        return null;
                }
-               Reply reply = new Reply(sone, post, System.currentTimeMillis(), text);
+               final Reply reply = new Reply(sone, post, System.currentTimeMillis(), text);
                synchronized (replies) {
                        replies.put(reply.getId(), reply);
                }
@@ -1797,7 +1733,17 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen
                        coreListenerManager.fireNewReplyFound(reply);
                }
                sone.addReply(reply);
-               saveSone(sone);
+               touchConfiguration();
+               localElementTicker.registerEvent(System.currentTimeMillis() + 10 * 1000, new Runnable() {
+
+                       /**
+                        * {@inheritDoc}
+                        */
+                       @Override
+                       public void run() {
+                               markReplyKnown(reply);
+                       }
+               }, "Mark " + reply + " read.");
                return reply;
        }
 
@@ -1821,7 +1767,7 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen
                        knownReplies.remove(reply.getId());
                }
                sone.removeReply(reply);
-               saveSone(sone);
+               touchConfiguration();
        }
 
        /**
@@ -1836,7 +1782,7 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen
                        if (newReplies.remove(reply.getId())) {
                                knownReplies.add(reply.getId());
                                coreListenerManager.fireMarkReplyKnown(reply);
-                               saveConfiguration();
+                               touchConfiguration();
                        }
                }
        }
@@ -1986,34 +1932,204 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen
        }
 
        /**
+        * Notifies the core that the configuration, either of the core or of a
+        * single local Sone, has changed, and that the configuration should be
+        * saved.
+        */
+       public void touchConfiguration() {
+               lastConfigurationUpdate = System.currentTimeMillis();
+       }
+
+       //
+       // SERVICE METHODS
+       //
+
+       /**
         * Starts the core.
         */
-       public void start() {
+       @Override
+       public void serviceStart() {
                loadConfiguration();
                updateChecker.addUpdateListener(this);
                updateChecker.start();
        }
 
        /**
+        * {@inheritDoc}
+        */
+       @Override
+       public void serviceRun() {
+               long lastSaved = System.currentTimeMillis();
+               while (!shouldStop()) {
+                       sleep(1000);
+                       long now = System.currentTimeMillis();
+                       if (shouldStop() || ((lastConfigurationUpdate > lastSaved) && ((now - lastConfigurationUpdate) > 5000))) {
+                               for (Sone localSone : getLocalSones()) {
+                                       saveSone(localSone);
+                               }
+                               saveConfiguration();
+                               lastSaved = now;
+                       }
+               }
+       }
+
+       /**
         * Stops the core.
         */
-       public void stop() {
+       @Override
+       public void serviceStop() {
                synchronized (localSones) {
                        for (SoneInserter soneInserter : soneInserters.values()) {
+                               soneInserter.removeSoneInsertListener(this);
                                soneInserter.stop();
                        }
                }
                updateChecker.stop();
                updateChecker.removeUpdateListener(this);
                soneDownloader.stop();
-               saveConfiguration();
-               stopped = true;
+       }
+
+       //
+       // PRIVATE METHODS
+       //
+
+       /**
+        * Saves the given Sone. This will persist all local settings for the given
+        * Sone, such as the friends list and similar, private options.
+        *
+        * @param sone
+        *            The Sone to save
+        */
+       private synchronized void saveSone(Sone sone) {
+               if (!isLocalSone(sone)) {
+                       logger.log(Level.FINE, "Tried to save non-local Sone: %s", sone);
+                       return;
+               }
+               if (!(sone.getIdentity() instanceof OwnIdentity)) {
+                       logger.log(Level.WARNING, "Local Sone without OwnIdentity found, refusing to save: %s", sone);
+                       return;
+               }
+
+               logger.log(Level.INFO, "Saving Sone: %s", sone);
+               try {
+                       ((OwnIdentity) sone.getIdentity()).setProperty("Sone.LatestEdition", String.valueOf(sone.getLatestEdition()));
+
+                       /* save Sone into configuration. */
+                       String sonePrefix = "Sone/" + sone.getId();
+                       configuration.getLongValue(sonePrefix + "/Time").setValue(sone.getTime());
+                       configuration.getStringValue(sonePrefix + "/LastInsertFingerprint").setValue(soneInserters.get(sone).getLastInsertFingerprint());
+
+                       /* save profile. */
+                       Profile profile = sone.getProfile();
+                       configuration.getStringValue(sonePrefix + "/Profile/FirstName").setValue(profile.getFirstName());
+                       configuration.getStringValue(sonePrefix + "/Profile/MiddleName").setValue(profile.getMiddleName());
+                       configuration.getStringValue(sonePrefix + "/Profile/LastName").setValue(profile.getLastName());
+                       configuration.getIntValue(sonePrefix + "/Profile/BirthDay").setValue(profile.getBirthDay());
+                       configuration.getIntValue(sonePrefix + "/Profile/BirthMonth").setValue(profile.getBirthMonth());
+                       configuration.getIntValue(sonePrefix + "/Profile/BirthYear").setValue(profile.getBirthYear());
+
+                       /* save profile fields. */
+                       int fieldCounter = 0;
+                       for (Field profileField : profile.getFields()) {
+                               String fieldPrefix = sonePrefix + "/Profile/Fields/" + fieldCounter++;
+                               configuration.getStringValue(fieldPrefix + "/Name").setValue(profileField.getName());
+                               configuration.getStringValue(fieldPrefix + "/Value").setValue(profileField.getValue());
+                       }
+                       configuration.getStringValue(sonePrefix + "/Profile/Fields/" + fieldCounter + "/Name").setValue(null);
+
+                       /* save posts. */
+                       int postCounter = 0;
+                       for (Post post : sone.getPosts()) {
+                               String postPrefix = sonePrefix + "/Posts/" + postCounter++;
+                               configuration.getStringValue(postPrefix + "/ID").setValue(post.getId());
+                               configuration.getStringValue(postPrefix + "/Recipient").setValue((post.getRecipient() != null) ? post.getRecipient().getId() : null);
+                               configuration.getLongValue(postPrefix + "/Time").setValue(post.getTime());
+                               configuration.getStringValue(postPrefix + "/Text").setValue(post.getText());
+                       }
+                       configuration.getStringValue(sonePrefix + "/Posts/" + postCounter + "/ID").setValue(null);
+
+                       /* save replies. */
+                       int replyCounter = 0;
+                       for (Reply reply : sone.getReplies()) {
+                               String replyPrefix = sonePrefix + "/Replies/" + replyCounter++;
+                               configuration.getStringValue(replyPrefix + "/ID").setValue(reply.getId());
+                               configuration.getStringValue(replyPrefix + "/Post/ID").setValue(reply.getPost().getId());
+                               configuration.getLongValue(replyPrefix + "/Time").setValue(reply.getTime());
+                               configuration.getStringValue(replyPrefix + "/Text").setValue(reply.getText());
+                       }
+                       configuration.getStringValue(sonePrefix + "/Replies/" + replyCounter + "/ID").setValue(null);
+
+                       /* save post likes. */
+                       int postLikeCounter = 0;
+                       for (String postId : sone.getLikedPostIds()) {
+                               configuration.getStringValue(sonePrefix + "/Likes/Post/" + postLikeCounter++ + "/ID").setValue(postId);
+                       }
+                       configuration.getStringValue(sonePrefix + "/Likes/Post/" + postLikeCounter + "/ID").setValue(null);
+
+                       /* save reply likes. */
+                       int replyLikeCounter = 0;
+                       for (String replyId : sone.getLikedReplyIds()) {
+                               configuration.getStringValue(sonePrefix + "/Likes/Reply/" + replyLikeCounter++ + "/ID").setValue(replyId);
+                       }
+                       configuration.getStringValue(sonePrefix + "/Likes/Reply/" + replyLikeCounter + "/ID").setValue(null);
+
+                       /* save friends. */
+                       int friendCounter = 0;
+                       for (String friendId : sone.getFriends()) {
+                               configuration.getStringValue(sonePrefix + "/Friends/" + friendCounter++ + "/ID").setValue(friendId);
+                       }
+                       configuration.getStringValue(sonePrefix + "/Friends/" + friendCounter + "/ID").setValue(null);
+
+                       /* save albums. first, collect in a flat structure, top-level first. */
+                       List<Album> albums = Sone.flattenAlbums(sone.getAlbums());
+
+                       int albumCounter = 0;
+                       for (Album album : albums) {
+                               String albumPrefix = sonePrefix + "/Albums/" + albumCounter++;
+                               configuration.getStringValue(albumPrefix + "/ID").setValue(album.getId());
+                               configuration.getStringValue(albumPrefix + "/Title").setValue(album.getTitle());
+                               configuration.getStringValue(albumPrefix + "/Description").setValue(album.getDescription());
+                               configuration.getStringValue(albumPrefix + "/Parent").setValue(album.getParent() == null ? null : album.getParent().getId());
+                       }
+                       configuration.getStringValue(sonePrefix + "/Albums/" + albumCounter + "/ID").setValue(null);
+
+                       /* save images. */
+                       int imageCounter = 0;
+                       for (Album album : albums) {
+                               for (Image image : album.getImages()) {
+                                       if (!image.isInserted()) {
+                                               continue;
+                                       }
+                                       String imagePrefix = sonePrefix + "/Images/" + imageCounter++;
+                                       configuration.getStringValue(imagePrefix + "/ID").setValue(image.getId());
+                                       configuration.getStringValue(imagePrefix + "/Album").setValue(album.getId());
+                                       configuration.getStringValue(imagePrefix + "/Key").setValue(image.getKey());
+                                       configuration.getStringValue(imagePrefix + "/Title").setValue(image.getTitle());
+                                       configuration.getStringValue(imagePrefix + "/Description").setValue(image.getDescription());
+                                       configuration.getLongValue(imagePrefix + "/CreationTime").setValue(image.getCreationTime());
+                                       configuration.getIntValue(imagePrefix + "/Width").setValue(image.getWidth());
+                                       configuration.getIntValue(imagePrefix + "/Height").setValue(image.getHeight());
+                               }
+                       }
+                       configuration.getStringValue(sonePrefix + "/Images/" + imageCounter + "/ID").setValue(null);
+
+                       /* save options. */
+                       configuration.getBooleanValue(sonePrefix + "/Options/AutoFollow").setValue(sone.getOptions().getBooleanOption("AutoFollow").getReal());
+                       configuration.getBooleanValue(sonePrefix + "/Options/EnableSoneInsertNotifications").setValue(sone.getOptions().getBooleanOption("EnableSoneInsertNotifications").getReal());
+
+                       configuration.save();
+                       logger.log(Level.INFO, "Sone %s saved.", sone);
+               } catch (ConfigurationException ce1) {
+                       logger.log(Level.WARNING, "Could not save Sone: " + sone, ce1);
+               } catch (WebOfTrustException wote1) {
+                       logger.log(Level.WARNING, "Could not set WoT property for Sone: " + sone, wote1);
+               }
        }
 
        /**
         * Saves the current options.
         */
-       public void saveConfiguration() {
+       private void saveConfiguration() {
                synchronized (configuration) {
                        if (storingConfiguration) {
                                logger.log(Level.FINE, "Already storing configuration…");
@@ -2027,9 +2143,13 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen
                        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/CharactersPerPost").setValue(options.getIntegerOption("CharactersPerPost").getReal());
+                       configuration.getBooleanValue("Option/RequireFullAccess").setValue(options.getBooleanOption("RequireFullAccess").getReal());
                        configuration.getIntValue("Option/PositiveTrust").setValue(options.getIntegerOption("PositiveTrust").getReal());
                        configuration.getIntValue("Option/NegativeTrust").setValue(options.getIntegerOption("NegativeTrust").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());
@@ -2082,17 +2202,13 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen
                }
        }
 
-       //
-       // PRIVATE METHODS
-       //
-
        /**
         * Loads the configuration.
         */
        @SuppressWarnings("unchecked")
        private void loadConfiguration() {
                /* create options. */
-               options.addIntegerOption("InsertionDelay", new DefaultOption<Integer>(60, new OptionWatcher<Integer>() {
+               options.addIntegerOption("InsertionDelay", new DefaultOption<Integer>(60, new IntegerRangeValidator(0, Integer.MAX_VALUE), new OptionWatcher<Integer>() {
 
                        @Override
                        public void optionChanged(Option<Integer> option, Integer oldValue, Integer newValue) {
@@ -2100,10 +2216,29 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen
                        }
 
                }));
-               options.addIntegerOption("PostsPerPage", new DefaultOption<Integer>(10));
-               options.addIntegerOption("PositiveTrust", new DefaultOption<Integer>(75));
-               options.addIntegerOption("NegativeTrust", new DefaultOption<Integer>(-25));
+               options.addIntegerOption("PostsPerPage", new DefaultOption<Integer>(10, new IntegerRangeValidator(1, Integer.MAX_VALUE)));
+               options.addIntegerOption("CharactersPerPost", 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.addIntegerOption("PositiveTrust", new DefaultOption<Integer>(75, new IntegerRangeValidator(0, 100)));
+               options.addIntegerOption("NegativeTrust", new DefaultOption<Integer>(-25, new IntegerRangeValidator(-100, 100)));
                options.addStringOption("TrustComment", new DefaultOption<String>("Set from Sone Web Interface"));
+               options.addBooleanOption("ActivateFcpInterface", new DefaultOption<Boolean>(false, new OptionWatcher<Boolean>() {
+
+                       @Override
+                       @SuppressWarnings("synthetic-access")
+                       public void optionChanged(Option<Boolean> option, Boolean oldValue, Boolean newValue) {
+                               fcpInterface.setActive(newValue);
+                       }
+               }));
+               options.addIntegerOption("FcpFullAccessRequired", new DefaultOption<Integer>(2, new OptionWatcher<Integer>() {
+
+                       @Override
+                       @SuppressWarnings("synthetic-access")
+                       public void optionChanged(Option<Integer> option, Integer oldValue, Integer newValue) {
+                               fcpInterface.setFullAccessRequired(FullAccessRequired.values()[newValue]);
+                       }
+
+               }));
                options.addBooleanOption("SoneRescueMode", new DefaultOption<Boolean>(false));
                options.addBooleanOption("ClearOnNextRestart", new DefaultOption<Boolean>(false));
                options.addBooleanOption("ReallyClearOnNextRestart", new DefaultOption<Boolean>(false));
@@ -2119,11 +2254,15 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen
                        return;
                }
 
-               options.getIntegerOption("InsertionDelay").set(configuration.getIntValue("Option/InsertionDelay").getValue(null));
-               options.getIntegerOption("PostsPerPage").set(configuration.getIntValue("Option/PostsPerPage").getValue(null));
-               options.getIntegerOption("PositiveTrust").set(configuration.getIntValue("Option/PositiveTrust").getValue(null));
-               options.getIntegerOption("NegativeTrust").set(configuration.getIntValue("Option/NegativeTrust").getValue(null));
+               loadConfigurationValue("InsertionDelay");
+               loadConfigurationValue("PostsPerPage");
+               loadConfigurationValue("CharactersPerPost");
+               options.getBooleanOption("RequireFullAccess").set(configuration.getBooleanValue("Option/RequireFullAccess").getValue(null));
+               loadConfigurationValue("PositiveTrust");
+               loadConfigurationValue("NegativeTrust");
                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. */
@@ -2177,6 +2316,21 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen
        }
 
        /**
+        * Loads an {@link Integer} configuration value for the option with the
+        * given name, logging validation failures.
+        *
+        * @param optionName
+        *            The name of the option to load
+        */
+       private void loadConfigurationValue(String optionName) {
+               try {
+                       options.getIntegerOption(optionName).set(configuration.getIntValue("Option/" + optionName).getValue(null));
+               } catch (IllegalArgumentException iae1) {
+                       logger.log(Level.WARNING, "Invalid value for " + optionName + " in configuration, using default.");
+               }
+       }
+
+       /**
         * Generate a Sone URI from the given URI and latest edition.
         *
         * @param uriString
@@ -2240,6 +2394,7 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen
                        public void run() {
                                Sone sone = getRemoteSone(identity.getId());
                                sone.setIdentity(identity);
+                               sone.setLatestEdition(Numbers.safeParseLong(identity.getProperty("Sone.LatestEdition"), sone.getLatestEdition()));
                                soneDownloader.addSone(sone);
                                soneDownloader.fetchSone(sone);
                        }
@@ -2252,6 +2407,49 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen
        @Override
        public void identityRemoved(OwnIdentity ownIdentity, Identity identity) {
                trustedIdentities.get(ownIdentity).remove(identity);
+               boolean foundIdentity = false;
+               for (Entry<OwnIdentity, Set<Identity>> trustedIdentity : trustedIdentities.entrySet()) {
+                       if (trustedIdentity.getKey().equals(ownIdentity)) {
+                               continue;
+                       }
+                       if (trustedIdentity.getValue().contains(identity)) {
+                               foundIdentity = true;
+                       }
+               }
+               if (foundIdentity) {
+                       /* some local identity still trusts this identity, don’t remove. */
+                       return;
+               }
+               Sone sone = getSone(identity.getId(), false);
+               if (sone == null) {
+                       /* TODO - we don’t have the Sone anymore. should this happen? */
+                       return;
+               }
+               synchronized (posts) {
+                       synchronized (newPosts) {
+                               for (Post post : sone.getPosts()) {
+                                       posts.remove(post.getId());
+                                       newPosts.remove(post.getId());
+                                       coreListenerManager.firePostRemoved(post);
+                               }
+                       }
+               }
+               synchronized (replies) {
+                       synchronized (newReplies) {
+                               for (Reply reply : sone.getReplies()) {
+                                       replies.remove(reply.getId());
+                                       newReplies.remove(reply.getId());
+                                       coreListenerManager.fireReplyRemoved(reply);
+                               }
+                       }
+               }
+               synchronized (remoteSones) {
+                       remoteSones.remove(identity.getId());
+               }
+               synchronized (newSones) {
+                       newSones.remove(identity.getId());
+                       coreListenerManager.fireSoneRemoved(sone);
+               }
        }
 
        //
@@ -2274,6 +2472,34 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen
         * {@inheritDoc}
         */
        @Override
+       public void insertStarted(Sone sone) {
+               coreListenerManager.fireSoneInserting(sone);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public void insertFinished(Sone sone, long insertDuration) {
+               coreListenerManager.fireSoneInserted(sone, insertDuration);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public void insertAborted(Sone sone, Throwable cause) {
+               coreListenerManager.fireSoneInsertAborted(sone, cause);
+       }
+
+       //
+       // SONEINSERTLISTENER METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
        public void imageInsertStarted(Image image) {
                logger.log(Level.WARNING, "Image insert started for " + image);
                coreListenerManager.fireImageInsertStarted(image);
@@ -2340,6 +2566,18 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen
                }
 
                /**
+                * Validates the given insertion delay.
+                *
+                * @param insertionDelay
+                *            The insertion delay to validate
+                * @return {@code true} if the given insertion delay was valid, {@code
+                *         false} otherwise
+                */
+               public boolean validateInsertionDelay(Integer insertionDelay) {
+                       return options.getIntegerOption("InsertionDelay").validate(insertionDelay);
+               }
+
+               /**
                 * Sets the insertion delay
                 *
                 * @param insertionDelay
@@ -2362,6 +2600,18 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen
                }
 
                /**
+                * Validates the number of posts per page.
+                *
+                * @param postsPerPage
+                *            The number of posts per page
+                * @return {@code true} if the number of posts per page was valid,
+                *         {@code false} otherwise
+                */
+               public boolean validatePostsPerPage(Integer postsPerPage) {
+                       return options.getIntegerOption("PostsPerPage").validate(postsPerPage);
+               }
+
+               /**
                 * Sets the number of posts to show per page.
                 *
                 * @param postsPerPage
@@ -2374,6 +2624,62 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen
                }
 
                /**
+                * Returns the number of characters per post, or <code>-1</code> if the
+                * posts should not be cut off.
+                *
+                * @return The numbers of characters per post
+                */
+               public int getCharactersPerPost() {
+                       return options.getIntegerOption("CharactersPerPost").get();
+               }
+
+               /**
+                * Validates the number of characters per post.
+                *
+                * @param charactersPerPost
+                *            The number of characters per post
+                * @return {@code true} if the number of characters per post was valid,
+                *         {@code false} otherwise
+                */
+               public boolean validateCharactersPerPost(Integer charactersPerPost) {
+                       return options.getIntegerOption("CharactersPerPost").validate(charactersPerPost);
+               }
+
+               /**
+                * Sets the number of characters per post.
+                *
+                * @param charactersPerPost
+                *            The number of characters per post, or <code>-1</code> to
+                *            not cut off the posts
+                * @return This preferences objects
+                */
+               public Preferences setCharactersPerPost(Integer charactersPerPost) {
+                       options.getIntegerOption("CharactersPerPost").set(charactersPerPost);
+                       return this;
+               }
+
+               /**
+                * Returns whether Sone requires full access to be even visible.
+                *
+                * @return {@code true} if Sone requires full access, {@code false}
+                *         otherwise
+                */
+               public boolean isRequireFullAccess() {
+                       return options.getBooleanOption("RequireFullAccess").get();
+               }
+
+               /**
+                * Sets whether Sone requires full access to be even visible.
+                *
+                * @param requireFullAccess
+                *            {@code true} if Sone requires full access, {@code false}
+                *            otherwise
+                */
+               public void setRequireFullAccess(Boolean requireFullAccess) {
+                       options.getBooleanOption("RequireFullAccess").set(requireFullAccess);
+               }
+
+               /**
                 * Returns the positive trust.
                 *
                 * @return The positive trust
@@ -2383,6 +2689,18 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen
                }
 
                /**
+                * Validates the positive trust.
+                *
+                * @param positiveTrust
+                *            The positive trust to validate
+                * @return {@code true} if the positive trust was valid, {@code false}
+                *         otherwise
+                */
+               public boolean validatePositiveTrust(Integer positiveTrust) {
+                       return options.getIntegerOption("PositiveTrust").validate(positiveTrust);
+               }
+
+               /**
                 * Sets the positive trust.
                 *
                 * @param positiveTrust
@@ -2405,6 +2723,18 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen
                }
 
                /**
+                * Validates the negative trust.
+                *
+                * @param negativeTrust
+                *            The negative trust to validate
+                * @return {@code true} if the negative trust was valid, {@code false}
+                *         otherwise
+                */
+               public boolean validateNegativeTrust(Integer negativeTrust) {
+                       return options.getIntegerOption("NegativeTrust").validate(negativeTrust);
+               }
+
+               /**
                 * Sets the negative trust.
                 *
                 * @param negativeTrust
@@ -2441,25 +2771,53 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen
                }
 
                /**
-                * Returns whether the rescue mode is active.
+                * Returns whether the {@link FcpInterface FCP interface} is currently
+                * active.
                 *
-                * @return {@code true} if the rescue mode is active, {@code false}
-                *         otherwise
+                * @see FcpInterface#setActive(boolean)
+                * @return {@code true} if the FCP interface is currently active,
+                *         {@code false} otherwise
                 */
-               public boolean isSoneRescueMode() {
-                       return options.getBooleanOption("SoneRescueMode").get();
+               public boolean isFcpInterfaceActive() {
+                       return options.getBooleanOption("ActivateFcpInterface").get();
                }
 
                /**
-                * Sets whether the rescue mode is active.
+                * Sets whether the {@link FcpInterface FCP interface} is currently
+                * active.
                 *
-                * @param soneRescueMode
-                *            {@code true} if the rescue mode is active, {@code false}
-                *            otherwise
+                * @see FcpInterface#setActive(boolean)
+                * @param fcpInterfaceActive
+                *            {@code true} to activate the FCP interface, {@code false}
+                *            to deactivate the FCP interface
+                * @return This preferences object
+                */
+               public Preferences setFcpInterfaceActive(boolean fcpInterfaceActive) {
+                       options.getBooleanOption("ActivateFcpInterface").set(fcpInterfaceActive);
+                       return this;
+               }
+
+               /**
+                * Returns the action level for which full access to the FCP interface
+                * is required.
+                *
+                * @return The action level for which full access to the FCP interface
+                *         is required
+                */
+               public FullAccessRequired getFcpFullAccessRequired() {
+                       return FullAccessRequired.values()[options.getIntegerOption("FcpFullAccessRequired").get()];
+               }
+
+               /**
+                * Sets the action level for which full access to the FCP interface is
+                * required
+                *
+                * @param fcpFullAccessRequired
+                *            The action level
                 * @return This preferences
                 */
-               public Preferences setSoneRescueMode(Boolean soneRescueMode) {
-                       options.getBooleanOption("SoneRescueMode").set(soneRescueMode);
+               public Preferences setFcpFullAccessRequired(FullAccessRequired fcpFullAccessRequired) {
+                       options.getIntegerOption("FcpFullAccessRequired").set((fcpFullAccessRequired != null) ? fcpFullAccessRequired.ordinal() : null);
                        return this;
                }
 
index 96a754f..1658745 100644 (file)
@@ -34,22 +34,6 @@ import net.pterodactylus.util.version.Version;
 public interface CoreListener extends EventListener {
 
        /**
-        * Notifies a listener that a Sone is now being rescued.
-        *
-        * @param sone
-        *            The Sone that is rescued
-        */
-       public void rescuingSone(Sone sone);
-
-       /**
-        * Notifies a listener that the Sone was rescued and can now be unlocked.
-        *
-        * @param sone
-        *            The Sone that was rescued
-        */
-       public void rescuedSone(Sone sone);
-
-       /**
         * Notifies a listener that a new Sone has been discovered.
         *
         * @param sone
@@ -98,6 +82,14 @@ public interface CoreListener extends EventListener {
        public void markReplyKnown(Reply reply);
 
        /**
+        * Notifies a listener that the given Sone was removed.
+        *
+        * @param sone
+        *            The removed Sone
+        */
+       public void soneRemoved(Sone sone);
+
+       /**
         * Notifies a listener that the given post was removed.
         *
         * @param post
@@ -130,6 +122,38 @@ public interface CoreListener extends EventListener {
        public void soneUnlocked(Sone sone);
 
        /**
+        * Notifies a listener that the insert of the given Sone has started.
+        *
+        * @see SoneInsertListener#insertStarted(Sone)
+        * @param sone
+        *            The Sone that is being inserted
+        */
+       public void soneInserting(Sone sone);
+
+       /**
+        * Notifies a listener that the insert of the given Sone has finished
+        * successfully.
+        *
+        * @see SoneInsertListener#insertFinished(Sone, long)
+        * @param sone
+        *            The Sone that has been inserted
+        * @param insertDuration
+        *            The insert duration (in milliseconds)
+        */
+       public void soneInserted(Sone sone, long insertDuration);
+
+       /**
+        * Notifies a listener that the insert of the given Sone was aborted.
+        *
+        * @see SoneInsertListener#insertAborted(Sone, Throwable)
+        * @param sone
+        *            The Sone that was inserted
+        * @param cause
+        *            The cause for the abortion (may be {@code null})
+        */
+       public void soneInsertAborted(Sone sone, Throwable cause);
+
+       /**
         * Notifies a listener that a new version has been found.
         *
         * @param version
index 1e050ea..5748ffc 100644 (file)
@@ -46,32 +46,6 @@ public class CoreListenerManager extends AbstractListenerManager<Core, CoreListe
        //
 
        /**
-        * Notifies all listeners that the given Sone is now being rescued.
-        *
-        * @see CoreListener#rescuingSone(Sone)
-        * @param sone
-        *            The Sone that is being rescued
-        */
-       void fireRescuingSone(Sone sone) {
-               for (CoreListener coreListener : getListeners()) {
-                       coreListener.rescuingSone(sone);
-               }
-       }
-
-       /**
-        * Notifies all listeners that the given Sone was rescued.
-        *
-        * @see CoreListener#rescuedSone(Sone)
-        * @param sone
-        *            The Sone that was rescued
-        */
-       void fireRescuedSone(Sone sone) {
-               for (CoreListener coreListener : getListeners()) {
-                       coreListener.rescuedSone(sone);
-               }
-       }
-
-       /**
         * Notifies all listeners that a new Sone has been discovered.
         *
         * @see CoreListener#newSoneFound(Sone)
@@ -148,6 +122,19 @@ public class CoreListenerManager extends AbstractListenerManager<Core, CoreListe
        }
 
        /**
+        * Notifies all listener that the given Sone was removed.
+        *
+        * @see CoreListener#soneRemoved(Sone)
+        * @param sone
+        *            The removed Sone
+        */
+       void fireSoneRemoved(Sone sone) {
+               for (CoreListener coreListener : getListeners()) {
+                       coreListener.soneRemoved(sone);
+               }
+       }
+
+       /**
         * Notifies all listener that the given post was removed.
         *
         * @see CoreListener#postRemoved(Post)
@@ -200,6 +187,50 @@ public class CoreListenerManager extends AbstractListenerManager<Core, CoreListe
        }
 
        /**
+        * Notifies all listeners that the insert of the given Sone has started.
+        *
+        * @see SoneInsertListener#insertStarted(Sone)
+        * @param sone
+        *            The Sone being inserted
+        */
+       void fireSoneInserting(Sone sone) {
+               for (CoreListener coreListener : getListeners()) {
+                       coreListener.soneInserting(sone);
+               }
+       }
+
+       /**
+        * Notifies all listeners that the insert of the given Sone has finished
+        * successfully.
+        *
+        * @see SoneInsertListener#insertFinished(Sone, long)
+        * @param sone
+        *            The Sone that was inserted
+        * @param insertDuration
+        *            The insert duration (in milliseconds)
+        */
+       void fireSoneInserted(Sone sone, long insertDuration) {
+               for (CoreListener coreListener : getListeners()) {
+                       coreListener.soneInserted(sone, insertDuration);
+               }
+       }
+
+       /**
+        * Notifies all listeners that the insert of the given Sone was aborted.
+        *
+        * @see SoneInsertListener#insertStarted(Sone)
+        * @param sone
+        *            The Sone being inserted
+        * @param cause
+        *            The cause for the abortion (may be {@code null}
+        */
+       void fireSoneInsertAborted(Sone sone, Throwable cause) {
+               for (CoreListener coreListener : getListeners()) {
+                       coreListener.soneInsertAborted(sone, cause);
+               }
+       }
+
+       /**
         * Notifies all listeners that a new version was found.
         *
         * @see CoreListener#updateFound(Version, long, long)
index 90383e1..39f342c 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * FreenetSone - FreenetInterface.java - Copyright © 2010 David Roden
+ * Sone - FreenetInterface.java - Copyright © 2010 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
index b7ece81..6a945b9 100644 (file)
@@ -1,3 +1,20 @@
+/*
+ * Sone - Options.java - Copyright © 2010 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.core;
 
 import java.util.ArrayList;
@@ -7,6 +24,8 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
+import net.pterodactylus.util.validation.Validator;
+
 /**
  * Stores various options that influence Sone’s behaviour.
  *
@@ -47,12 +66,26 @@ public class Options {
                public T getReal();
 
                /**
+                * Validates the given value. Note that {@code null} is always a valid
+                * value!
+                *
+                * @param value
+                *            The value to validate
+                * @return {@code true} if this option does not have a {@link Validator}
+                *         , or the {@link Validator} validates this object, {@code
+                *         false} otherwise
+                */
+               public boolean validate(T value);
+
+               /**
                 * Sets the current value of the option.
                 *
                 * @param value
                 *            The new value of the option
+                * @throws IllegalArgumentException
+                *             if the value is not valid for this option
                 */
-               public void set(T value);
+               public void set(T value) throws IllegalArgumentException;
 
        }
 
@@ -96,6 +129,9 @@ public class Options {
                /** The current value. */
                private volatile T value;
 
+               /** The validator. */
+               private Validator<T> validator;
+
                /** The option watcher. */
                private final List<OptionWatcher<T>> optionWatchers = new ArrayList<OptionWatcher<T>>();
 
@@ -108,7 +144,22 @@ public class Options {
                 *            The option watchers
                 */
                public DefaultOption(T defaultValue, OptionWatcher<T>... optionWatchers) {
+                       this(defaultValue, null, optionWatchers);
+               }
+
+               /**
+                * Creates a new default option.
+                *
+                * @param defaultValue
+                *            The default value of the option
+                * @param validator
+                *            The validator for value validation
+                * @param optionWatchers
+                *            The option watchers
+                */
+               public DefaultOption(T defaultValue, Validator<T> validator, OptionWatcher<T>... optionWatchers) {
                        this.defaultValue = defaultValue;
+                       this.validator = validator;
                        this.optionWatchers.addAll(Arrays.asList(optionWatchers));
                }
 
@@ -143,7 +194,18 @@ public class Options {
                 * {@inheritDoc}
                 */
                @Override
+               public boolean validate(@SuppressWarnings("hiding") T value) {
+                       return (validator == null) || (value == null) || validator.validate(value);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               @Override
                public void set(T value) {
+                       if ((value != null) && (validator != null) && (!validator.validate(value))) {
+                               throw new IllegalArgumentException("New Value (" + value + ") could not be validated.");
+                       }
                        T oldValue = this.value;
                        this.value = value;
                        if (!get().equals(oldValue)) {
diff --git a/src/main/java/net/pterodactylus/sone/core/PostProvider.java b/src/main/java/net/pterodactylus/sone/core/PostProvider.java
new file mode 100644 (file)
index 0000000..edede44
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ * Sone - PostProvider.java - Copyright © 2011 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.core;
+
+import net.pterodactylus.sone.data.Post;
+
+/**
+ * Interface for objects that can provide {@link Post}s by their ID.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface PostProvider {
+
+       /**
+        * Returns the post with the given ID, if it exists. If it does not exist
+        * and {@code create} is {@code false}, {@code null} is returned; otherwise,
+        * a new post with the given ID is created and returned.
+        *
+        * @param postId
+        *            The ID of the post to return
+        * @param create
+        *            {@code true} to create a new post if no post with the given ID
+        *            exists, {@code false} to return {@code null} instead
+        * @return The post with the given ID, or {@code null}
+        */
+       public Post getPost(String postId, boolean create);
+
+}
index 8d0d1f2..4da1226 100644 (file)
@@ -26,7 +26,6 @@ import java.util.Set;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
-import net.pterodactylus.sone.core.Core.Preferences;
 import net.pterodactylus.sone.core.Core.SoneStatus;
 import net.pterodactylus.sone.data.Album;
 import net.pterodactylus.sone.data.Client;
@@ -122,13 +121,12 @@ public class SoneDownloader extends AbstractService {
         *            The Sone to fetch
         */
        public void fetchSone(Sone sone) {
-               fetchSone(sone, sone.getRequestUri());
+               fetchSone(sone, sone.getRequestUri().sskForUSK());
        }
 
        /**
         * Fetches the updated Sone. This method can be used to fetch a Sone from a
-        * specific URI (which happens when {@link Preferences#isSoneRescueMode()
-        * „Sone rescue mode“} is active).
+        * specific URI.
         *
         * @param sone
         *            The Sone to fetch
@@ -136,6 +134,23 @@ public class SoneDownloader extends AbstractService {
         *            The URI to fetch the Sone from
         */
        public void fetchSone(Sone sone, FreenetURI soneUri) {
+               fetchSone(sone, soneUri, false);
+       }
+
+       /**
+        * Fetches the Sone from the given URI.
+        *
+        * @param sone
+        *            The Sone to fetch
+        * @param soneUri
+        *            The URI of the Sone to fetch
+        * @param fetchOnly
+        *            {@code true} to only fetch and parse the Sone, {@code false}
+        *            to {@link Core#updateSone(Sone) update} it in the core
+        * @return The downloaded Sone, or {@code null} if the Sone could not be
+        *         downloaded
+        */
+       public Sone fetchSone(Sone sone, FreenetURI soneUri, boolean fetchOnly) {
                logger.log(Level.FINE, "Starting fetch for Sone “%s” from %s…", new Object[] { sone, soneUri });
                FreenetURI requestUri = soneUri.setMetaString(new String[] { "sone.xml" });
                core.setSoneStatus(sone, SoneStatus.downloading);
@@ -143,14 +158,17 @@ public class SoneDownloader extends AbstractService {
                        Pair<FreenetURI, FetchResult> fetchResults = freenetInterface.fetchUri(requestUri);
                        if (fetchResults == null) {
                                /* TODO - mark Sone as bad. */
-                               return;
+                               return null;
                        }
                        logger.log(Level.FINEST, "Got %d bytes back.", fetchResults.getRight().size());
                        Sone parsedSone = parseSone(sone, fetchResults.getRight(), fetchResults.getLeft());
                        if (parsedSone != null) {
-                               addSone(parsedSone);
-                               core.updateSone(parsedSone);
+                               if (!fetchOnly) {
+                                       core.updateSone(parsedSone);
+                                       addSone(parsedSone);
+                               }
                        }
+                       return parsedSone;
                } finally {
                        core.setSoneStatus(sone, (sone.getTime() == 0) ? SoneStatus.unknown : SoneStatus.idle);
                }
index efe28f5..271627e 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * FreenetSone - SoneException.java - Copyright © 2010 David Roden
+ * Sone - SoneException.java - Copyright © 2010 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
diff --git a/src/main/java/net/pterodactylus/sone/core/SoneInsertListener.java b/src/main/java/net/pterodactylus/sone/core/SoneInsertListener.java
new file mode 100644 (file)
index 0000000..6391d8a
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ * Sone - SoneInsertListener.java - Copyright © 2011 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.core;
+
+import java.util.EventListener;
+
+import net.pterodactylus.sone.data.Sone;
+
+/**
+ * Listener for Sone insert events.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface SoneInsertListener extends EventListener {
+
+       /**
+        * Notifies a listener that a Sone is now being inserted.
+        *
+        * @param sone
+        *            The Sone being inserted
+        */
+       public void insertStarted(Sone sone);
+
+       /**
+        * Notifies a listener that a Sone has been successfully inserted.
+        *
+        * @param sone
+        *            The Sone that was inserted
+        * @param insertDuration
+        *            The duration of the insert (in milliseconds)
+        */
+       public void insertFinished(Sone sone, long insertDuration);
+
+       /**
+        * Notifies a listener that the insert of the given Sone was aborted.
+        *
+        * @param sone
+        *            The Sone that was being inserted
+        * @param cause
+        *            The cause of the abortion (may be {@code null})
+        */
+       public void insertAborted(Sone sone, Throwable cause);
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/core/SoneInsertListenerManager.java b/src/main/java/net/pterodactylus/sone/core/SoneInsertListenerManager.java
new file mode 100644 (file)
index 0000000..9e4ea2a
--- /dev/null
@@ -0,0 +1,82 @@
+/*
+ * Sone - SoneInsertListenerManager.java - Copyright © 2011 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.core;
+
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.util.event.AbstractListenerManager;
+
+/**
+ * Manager for {@link SoneInsertListener}s.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class SoneInsertListenerManager extends AbstractListenerManager<Sone, SoneInsertListener> {
+
+       /**
+        * Creates a new Sone insert listener manager.
+        *
+        * @param sone
+        *            The sone being inserted
+        */
+       public SoneInsertListenerManager(Sone sone) {
+               super(sone);
+       }
+
+       //
+       // ACTIONS
+       //
+
+       /**
+        * Notifies all listeners that the insert of the Sone has started.
+        *
+        * @see SoneInsertListener#insertStarted(Sone)
+        */
+       void fireInsertStarted() {
+               for (SoneInsertListener soneInsertListener : getListeners()) {
+                       soneInsertListener.insertStarted(getSource());
+               }
+       }
+
+       /**
+        * Notifies all listeners that the insert of the Sone has finished
+        * successfully.
+        *
+        * @see SoneInsertListener#insertFinished(Sone, long)
+        * @param insertDuration
+        *            The insert duration (in milliseconds)
+        */
+       void fireInsertFinished(long insertDuration) {
+               for (SoneInsertListener soneInsertListener : getListeners()) {
+                       soneInsertListener.insertFinished(getSource(), insertDuration);
+               }
+       }
+
+       /**
+        * Notifies all listeners that the insert of the Sone was aborted.
+        *
+        * @see SoneInsertListener#insertAborted(Sone, Throwable)
+        * @param cause
+        *            The cause of the abortion (may be {@code null}
+        */
+       void fireInsertAborted(Throwable cause) {
+               for (SoneInsertListener soneInsertListener : getListeners()) {
+                       soneInsertListener.insertAborted(getSource(), cause);
+               }
+       }
+
+}
index 2160ab8..44f8bbc 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * FreenetSone - SoneInserter.java - Copyright © 2010 David Roden
+ * Sone - SoneInserter.java - Copyright © 2010 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
@@ -33,6 +33,8 @@ import net.pterodactylus.sone.data.Reply;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.freenet.StringBucket;
 import net.pterodactylus.sone.main.SonePlugin;
+import net.pterodactylus.util.collection.ListBuilder;
+import net.pterodactylus.util.collection.ReverseComparator;
 import net.pterodactylus.util.io.Closer;
 import net.pterodactylus.util.logging.Logging;
 import net.pterodactylus.util.service.AbstractService;
@@ -81,6 +83,9 @@ public class SoneInserter extends AbstractService {
        /** The Sone to insert. */
        private final Sone sone;
 
+       /** The insert listener manager. */
+       private SoneInsertListenerManager soneInsertListenerManager;
+
        /** Whether a modification has been detected. */
        private volatile boolean modified = false;
 
@@ -102,6 +107,31 @@ public class SoneInserter extends AbstractService {
                this.core = core;
                this.freenetInterface = freenetInterface;
                this.sone = sone;
+               this.soneInsertListenerManager = new SoneInsertListenerManager(sone);
+       }
+
+       //
+       // LISTENER MANAGEMENT
+       //
+
+       /**
+        * Adds a listener for Sone insert events.
+        *
+        * @param soneInsertListener
+        *            The Sone insert listener
+        */
+       public void addSoneInsertListener(SoneInsertListener soneInsertListener) {
+               soneInsertListenerManager.addListener(soneInsertListener);
+       }
+
+       /**
+        * Removes a listener for Sone insert events.
+        *
+        * @param soneInsertListener
+        *            The Sone insert listener
+        */
+       public void removeSoneInsertListener(SoneInsertListener soneInsertListener) {
+               soneInsertListenerManager.removeListener(soneInsertListener);
        }
 
        //
@@ -204,7 +234,9 @@ public class SoneInserter extends AbstractService {
                                        core.setSoneStatus(sone, SoneStatus.inserting);
                                        long insertTime = System.currentTimeMillis();
                                        insertInformation.setTime(insertTime);
+                                       soneInsertListenerManager.fireInsertStarted();
                                        FreenetURI finalUri = freenetInterface.insertDirectory(insertInformation.getInsertUri(), insertInformation.generateManifestEntries(), "index.html");
+                                       soneInsertListenerManager.fireInsertFinished(System.currentTimeMillis() - insertTime);
                                        /* at this point we might already be stopped. */
                                        if (shouldStop()) {
                                                /* if so, bail out, don’t change anything. */
@@ -212,10 +244,11 @@ public class SoneInserter extends AbstractService {
                                        }
                                        sone.setTime(insertTime);
                                        sone.setLatestEdition(finalUri.getEdition());
-                                       core.saveSone(sone);
+                                       core.touchConfiguration();
                                        success = true;
                                        logger.log(Level.INFO, "Inserted Sone “%s” at %s.", new Object[] { sone.getName(), finalUri });
                                } catch (SoneException se1) {
+                                       soneInsertListenerManager.fireInsertAborted(se1);
                                        logger.log(Level.WARNING, "Could not insert Sone “" + sone.getName() + "”!", se1);
                                } finally {
                                        core.setSoneStatus(sone, SoneStatus.idle);
@@ -229,7 +262,6 @@ public class SoneInserter extends AbstractService {
                                        synchronized (sone) {
                                                if (lastInsertFingerprint.equals(sone.getFingerprint())) {
                                                        logger.log(Level.FINE, "Sone “%s” was not modified further, resetting counter…", new Object[] { sone });
-                                                       core.saveSone(sone);
                                                        lastModificationTime = 0;
                                                        modified = false;
                                                }
@@ -248,7 +280,7 @@ public class SoneInserter extends AbstractService {
         *
         * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
         */
-       private static class InsertInformation {
+       private class InsertInformation {
 
                /** All properties of the Sone, copied for thread safety. */
                private final Map<String, Object> soneProperties = new HashMap<String, Object>();
@@ -266,8 +298,8 @@ public class SoneInserter extends AbstractService {
                        soneProperties.put("requestUri", sone.getRequestUri());
                        soneProperties.put("insertUri", sone.getInsertUri());
                        soneProperties.put("profile", sone.getProfile());
-                       soneProperties.put("posts", new ArrayList<Post>(sone.getPosts()));
-                       soneProperties.put("replies", new HashSet<Reply>(sone.getReplies()));
+                       soneProperties.put("posts", new ListBuilder<Post>(new ArrayList<Post>(sone.getPosts())).sort(Post.TIME_COMPARATOR).get());
+                       soneProperties.put("replies", new ListBuilder<Reply>(new ArrayList<Reply>(sone.getReplies())).sort(new ReverseComparator<Reply>(Reply.TIME_COMPARATOR)).get());
                        soneProperties.put("likedPostIds", new HashSet<String>(sone.getLikedPostIds()));
                        soneProperties.put("likedReplyIds", new HashSet<String>(sone.getLikedReplyIds()));
                        soneProperties.put("albums", Sone.flattenAlbums(sone.getAlbums()));
@@ -347,7 +379,9 @@ public class SoneInserter extends AbstractService {
                        }
 
                        TemplateContext templateContext = templateContextFactory.createTemplateContext();
+                       templateContext.set("core", core);
                        templateContext.set("currentSone", soneProperties);
+                       templateContext.set("currentEdition", core.getUpdateChecker().getLatestEdition());
                        templateContext.set("version", SonePlugin.VERSION);
                        StringWriter writer = new StringWriter();
                        StringBucket bucket = null;
diff --git a/src/main/java/net/pterodactylus/sone/core/SoneProvider.java b/src/main/java/net/pterodactylus/sone/core/SoneProvider.java
new file mode 100644 (file)
index 0000000..dcf0f61
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ * Sone - SoneProvider.java - Copyright © 2011 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.core;
+
+import net.pterodactylus.sone.data.Sone;
+
+/**
+ * Interface for objects that can provide {@link Sone}s by their ID.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface SoneProvider {
+
+       /**
+        * Returns the Sone with the given ID, if it exists. If it does not exist
+        * and {@code create} is {@code false}, {@code null} is returned; otherwise,
+        * a new Sone with the given ID is created and returned.
+        *
+        * @param soneId
+        *            The ID of the Sone to return
+        * @param create
+        *            {@code true} to create a new Sone if no Sone with the given ID
+        *            exists, {@code false} to return {@code null} instead
+        * @return The Sone with the given ID, or {@code null}
+        */
+       public Sone getSone(String soneId, boolean create);
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/core/SoneRescuer.java b/src/main/java/net/pterodactylus/sone/core/SoneRescuer.java
new file mode 100644 (file)
index 0000000..542ec4a
--- /dev/null
@@ -0,0 +1,173 @@
+/*
+ * Sone - SoneRescuer.java - Copyright © 2011 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.core;
+
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.util.service.AbstractService;
+import freenet.keys.FreenetURI;
+
+/**
+ * The Sone rescuer downloads older editions of a Sone and updates the currently
+ * stored Sone with it.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class SoneRescuer extends AbstractService {
+
+       /** The core. */
+       private final Core core;
+
+       /** The Sone downloader. */
+       private final SoneDownloader soneDownloader;
+
+       /** The Sone being rescued. */
+       private final Sone sone;
+
+       /** Whether the rescuer is currently fetching a Sone. */
+       private volatile boolean fetching;
+
+       /** The currently tried edition. */
+       private volatile long currentEdition;
+
+       /** Whether the last fetch was successful. */
+       private volatile boolean lastFetchSuccessful = true;
+
+       /**
+        * Creates a new Sone rescuer.
+        *
+        * @param core
+        *            The core
+        * @param soneDownloader
+        *            The Sone downloader
+        * @param sone
+        *            The Sone to rescue
+        */
+       public SoneRescuer(Core core, SoneDownloader soneDownloader, Sone sone) {
+               super("Sone Rescuer for " + sone.getName());
+               this.core = core;
+               this.soneDownloader = soneDownloader;
+               this.sone = sone;
+               currentEdition = sone.getRequestUri().getEdition();
+       }
+
+       //
+       // ACCESSORS
+       //
+
+       /**
+        * Returns whether the Sone rescuer is currently fetching a Sone.
+        *
+        * @return {@code true} if the Sone rescuer is currently fetching a Sone
+        */
+       public boolean isFetching() {
+               return fetching;
+       }
+
+       /**
+        * Returns the edition that is currently being downloaded.
+        *
+        * @return The edition that is currently being downloaded
+        */
+       public long getCurrentEdition() {
+               return currentEdition;
+       }
+
+       /**
+        * Returns whether the Sone rescuer can download a next edition.
+        *
+        * @return {@code true} if the Sone rescuer can download a next edition,
+        *         {@code false} if the last edition was already tried
+        */
+       public boolean hasNextEdition() {
+               return currentEdition > 0;
+       }
+
+       /**
+        * Returns the next edition the Sone rescuer can download.
+        *
+        * @return The next edition the Sone rescuer can download
+        */
+       public long getNextEdition() {
+               return currentEdition - 1;
+       }
+
+       /**
+        * Sets the edition to rescue.
+        *
+        * @param edition
+        *            The edition to rescue
+        * @return This Sone rescuer
+        */
+       public SoneRescuer setEdition(long edition) {
+               currentEdition = edition;
+               return this;
+       }
+
+       /**
+        * Sets whether the last fetch was successful.
+        *
+        * @return {@code true} if the last fetch was successful, {@code false}
+        *         otherwise
+        */
+       public boolean isLastFetchSuccessful() {
+               return lastFetchSuccessful;
+       }
+
+       //
+       // ACTIONS
+       //
+
+       /**
+        * Starts the next fetch. If you want to fetch a different edition than “the
+        * next older one,” remember to call {@link #setEdition(long)} before
+        * calling this method.
+        */
+       public void startNextFetch() {
+               fetching = true;
+               notifySyncObject();
+       }
+
+       //
+       // SERVICE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void serviceRun() {
+               while (!shouldStop()) {
+                       while (!shouldStop() && !fetching) {
+                               sleep();
+                       }
+                       if (fetching) {
+                               core.lockSone(sone);
+                               FreenetURI soneUri = sone.getRequestUri().setKeyType("SSK").setDocName("Sone-" + currentEdition).setMetaString(new String[] { "sone.xml" });
+                               System.out.println("URI: " + soneUri);
+                               Sone fetchedSone = soneDownloader.fetchSone(sone, soneUri, true);
+                               System.out.println("Sone: " + fetchedSone);
+                               lastFetchSuccessful = (fetchedSone != null);
+                               if (lastFetchSuccessful) {
+                                       core.updateSone(fetchedSone, true);
+                               }
+                               fetching = false;
+                       }
+               }
+       }
+
+}
index e07e160..9c50f37 100644 (file)
@@ -49,7 +49,7 @@ public class UpdateChecker {
        private static final String SONE_HOMEPAGE = "USK@nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI,DuQSUZiI~agF8c-6tjsFFGuZ8eICrzWCILB60nT8KKo,AQACAAE/sone/";
 
        /** The current latest known edition. */
-       private static final int LATEST_EDITION = 25;
+       private static final int LATEST_EDITION = 36;
 
        /** The Freenet interface. */
        private final FreenetInterface freenetInterface;
index bb431d0..a28bf9f 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * FreenetSone - StatusUpdate.java - Copyright © 2010 David Roden
+ * Sone - Post.java - Copyright © 2010 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
index 8d4306d..6b44d10 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * FreenetSone - Profile.java - Copyright © 2010 David Roden
+ * Sone - Profile.java - Copyright © 2010 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
index 3aa604e..b8db269 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * FreenetSone - Sone.java - Copyright © 2010 David Roden
+ * Sone - Sone.java - Copyright © 2010 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
@@ -27,8 +27,10 @@ import java.util.Set;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
+import net.pterodactylus.sone.core.Core;
 import net.pterodactylus.sone.core.Options;
 import net.pterodactylus.sone.freenet.wot.Identity;
+import net.pterodactylus.sone.freenet.wot.OwnIdentity;
 import net.pterodactylus.sone.template.SoneAccessor;
 import net.pterodactylus.util.filter.Filter;
 import net.pterodactylus.util.logging.Logging;
@@ -59,6 +61,29 @@ public class Sone implements Fingerprintable, Comparable<Sone> {
 
        };
 
+       /**
+        * Comparator that sorts Sones by last activity (least recent active first).
+        */
+       public static final Comparator<Sone> LAST_ACTIVITY_COMPARATOR = new Comparator<Sone>() {
+
+               @Override
+               public int compare(Sone firstSone, Sone secondSone) {
+                       return (int) Math.min(Integer.MAX_VALUE, Math.max(Integer.MIN_VALUE, secondSone.getTime() - firstSone.getTime()));
+               }
+       };
+
+       /** Comparator that sorts Sones by numbers of posts (descending). */
+       public static final Comparator<Sone> POST_COUNT_COMPARATOR = new Comparator<Sone>() {
+
+               /**
+                * {@inheritDoc}
+                */
+               @Override
+               public int compare(Sone leftSone, Sone rightSone) {
+                       return (leftSone.getPosts().size() != rightSone.getPosts().size()) ? (rightSone.getPosts().size() - leftSone.getPosts().size()) : (rightSone.getReplies().size() - leftSone.getReplies().size());
+               }
+       };
+
        /** Filter to remove Sones that have not been downloaded. */
        public static final Filter<Sone> EMPTY_SONE_FILTER = new Filter<Sone>() {
 
@@ -68,6 +93,16 @@ public class Sone implements Fingerprintable, Comparable<Sone> {
                }
        };
 
+       /** Filter that matches all {@link Core#isLocalSone(Sone) local Sones}. */
+       public static final Filter<Sone> LOCAL_SONE_FILTER = new Filter<Sone>() {
+
+               @Override
+               public boolean filterObject(Sone sone) {
+                       return sone.getIdentity() instanceof OwnIdentity;
+               }
+
+       };
+
        /** The logger. */
        private static final Logger logger = Logging.getLogger(Sone.class);
 
@@ -672,6 +707,7 @@ public class Sone implements Fingerprintable, Comparable<Sone> {
                }
                fingerprint.append(")");
 
+               @SuppressWarnings("hiding")
                List<Reply> replies = new ArrayList<Reply>(getReplies());
                Collections.sort(replies, Reply.TIME_COMPARATOR);
                fingerprint.append("Replies(");
@@ -680,6 +716,7 @@ public class Sone implements Fingerprintable, Comparable<Sone> {
                }
                fingerprint.append(')');
 
+               @SuppressWarnings("hiding")
                List<String> likedPostIds = new ArrayList<String>(getLikedPostIds());
                Collections.sort(likedPostIds);
                fingerprint.append("LikedPosts(");
@@ -688,6 +725,7 @@ public class Sone implements Fingerprintable, Comparable<Sone> {
                }
                fingerprint.append(')');
 
+               @SuppressWarnings("hiding")
                List<String> likedReplyIds = new ArrayList<String>(getLikedReplyIds());
                Collections.sort(likedReplyIds);
                fingerprint.append("LikedReplies(");
diff --git a/src/main/java/net/pterodactylus/sone/fcp/AbstractSoneCommand.java b/src/main/java/net/pterodactylus/sone/fcp/AbstractSoneCommand.java
new file mode 100644 (file)
index 0000000..d88872e
--- /dev/null
@@ -0,0 +1,406 @@
+/*
+ * Sone - FcpInterface.java - Copyright © 2011 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.fcp;
+
+import java.util.Collection;
+import java.util.List;
+
+import net.pterodactylus.sone.core.Core;
+import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.sone.data.Profile;
+import net.pterodactylus.sone.data.Reply;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.data.Profile.Field;
+import net.pterodactylus.sone.freenet.SimpleFieldSetBuilder;
+import net.pterodactylus.sone.freenet.fcp.AbstractCommand;
+import net.pterodactylus.sone.freenet.fcp.Command;
+import net.pterodactylus.sone.freenet.fcp.FcpException;
+import net.pterodactylus.sone.template.SoneAccessor;
+import net.pterodactylus.util.filter.Filters;
+import freenet.node.FSParseException;
+import freenet.support.SimpleFieldSet;
+
+/**
+ * Abstract base implementation of a {@link Command} with Sone-related helper
+ * methods.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public abstract class AbstractSoneCommand extends AbstractCommand {
+
+       /** The Sone core. */
+       private final Core core;
+
+       /** Whether this command needs write access. */
+       private final boolean writeAccess;
+
+       /**
+        * Creates a new abstract Sone FCP command.
+        *
+        * @param core
+        *            The Sone core
+        */
+       protected AbstractSoneCommand(Core core) {
+               this(core, false);
+       }
+
+       /**
+        * Creates a new abstract Sone FCP command.
+        *
+        * @param core
+        *            The Sone core
+        * @param writeAccess
+        *            {@code true} if this command requires write access,
+        *            {@code false} otherwise
+        */
+       protected AbstractSoneCommand(Core core, boolean writeAccess) {
+               this.core = core;
+               this.writeAccess = writeAccess;
+       }
+
+       //
+       // ACCESSORS
+       //
+
+       /**
+        * Returns the Sone core.
+        *
+        * @return The Sone core
+        */
+       protected Core getCore() {
+               return core;
+       }
+
+       /**
+        * Returns whether this command requires write access.
+        *
+        * @return {@code true} if this command require write access, {@code false}
+        *         otherwise
+        */
+       public boolean requiresWriteAccess() {
+               return writeAccess;
+       }
+
+       //
+       // PROTECTED METHODS
+       //
+
+       /**
+        * Encodes text in a way that makes it possible for the text to be stored in
+        * a {@link SimpleFieldSet}. Backslashes, CR, and LF are prepended with a
+        * backslash.
+        *
+        * @param text
+        *            The text to encode
+        * @return The encoded text
+        */
+       protected String encodeString(String text) {
+               return text.replaceAll("\\\\", "\\\\").replaceAll("\n", "\\\\n").replaceAll("\r", "\\\\r");
+       }
+
+       /**
+        * Returns a Sone whose ID is a parameter in the given simple field set.
+        *
+        * @param simpleFieldSet
+        *            The simple field set containing the ID of the Sone
+        * @param parameterName
+        *            The name under which the Sone ID is stored in the simple field
+        *            set
+        * @param localOnly
+        *            {@code true} to only return local Sones, {@code false} to
+        *            return any Sones
+        * @return The Sone
+        * @throws FcpException
+        *             if there is no Sone ID stored under the given parameter name,
+        *             or if the Sone ID is invalid
+        */
+       protected Sone getSone(SimpleFieldSet simpleFieldSet, String parameterName, boolean localOnly) throws FcpException {
+               return getSone(simpleFieldSet, parameterName, localOnly, true);
+       }
+
+       /**
+        * Returns a Sone whose ID is a parameter in the given simple field set.
+        *
+        * @param simpleFieldSet
+        *            The simple field set containing the ID of the Sone
+        * @param parameterName
+        *            The name under which the Sone ID is stored in the simple field
+        *            set
+        * @param localOnly
+        *            {@code true} to only return local Sones, {@code false} to
+        *            return any Sones
+        * @param mandatory
+        *            {@code true} if a valid Sone ID is required, {@code false}
+        *            otherwise
+        * @return The Sone, or {@code null} if {@code mandatory} is {@code false}
+        *         and the Sone ID is invalid
+        * @throws FcpException
+        *             if there is no Sone ID stored under the given parameter name,
+        *             or if {@code mandatory} is {@code true} and the Sone ID is
+        *             invalid
+        */
+       protected Sone getSone(SimpleFieldSet simpleFieldSet, String parameterName, boolean localOnly, boolean mandatory) throws FcpException {
+               String soneId = simpleFieldSet.get(parameterName);
+               if (mandatory && (soneId == null)) {
+                       throw new FcpException("Could not load Sone ID from “" + parameterName + "”.");
+               }
+               Sone sone = localOnly ? core.getLocalSone(soneId, false) : core.getSone(soneId, false);
+               if (mandatory && (sone == null)) {
+                       throw new FcpException("Could not load Sone from “" + soneId + "”.");
+               }
+               return sone;
+       }
+
+       /**
+        * Returns a post whose ID is a parameter in the given simple field set.
+        *
+        * @param simpleFieldSet
+        *            The simple field set containing the ID of the post
+        * @param parameterName
+        *            The name under which the post ID is stored in the simple field
+        *            set
+        * @return The post
+        * @throws FcpException
+        *             if there is no post ID stored under the given parameter name,
+        *             or if the post ID is invalid
+        */
+       protected Post getPost(SimpleFieldSet simpleFieldSet, String parameterName) throws FcpException {
+               try {
+                       String postId = simpleFieldSet.getString(parameterName);
+                       Post post = core.getPost(postId, false);
+                       if (post == null) {
+                               throw new FcpException("Could not load post from “" + postId + "”.");
+                       }
+                       return post;
+               } catch (FSParseException fspe1) {
+                       throw new FcpException("Could not post ID from “" + parameterName + "”.", fspe1);
+               }
+       }
+
+       /**
+        * Returns a reply whose ID is a parameter in the given simple field set.
+        *
+        * @param simpleFieldSet
+        *            The simple field set containing the ID of the reply
+        * @param parameterName
+        *            The name under which the reply ID is stored in the simple
+        *            field set
+        * @return The reply
+        * @throws FcpException
+        *             if there is no reply ID stored under the given parameter
+        *             name, or if the reply ID is invalid
+        */
+       protected Reply getReply(SimpleFieldSet simpleFieldSet, String parameterName) throws FcpException {
+               try {
+                       String replyId = simpleFieldSet.getString(parameterName);
+                       Reply reply = core.getReply(replyId, false);
+                       if (reply == null) {
+                               throw new FcpException("Could not load reply from “" + replyId + "”.");
+                       }
+                       return reply;
+               } catch (FSParseException fspe1) {
+                       throw new FcpException("Could not reply ID from “" + parameterName + "”.", fspe1);
+               }
+       }
+
+       /**
+        * Creates a simple field set from the given Sone, including {@link Profile}
+        * information.
+        *
+        * @param sone
+        *            The Sone to encode
+        * @param prefix
+        *            The prefix for the field names (may be empty but not {@code
+        *            null})
+        * @param localSone
+        *            An optional local Sone that is used for Sone-specific data,
+        *            such as if the Sone is followed by the local Sone
+        * @return The simple field set containing the given Sone
+        */
+       protected SimpleFieldSet encodeSone(Sone sone, String prefix, Sone localSone) {
+               SimpleFieldSetBuilder soneBuilder = new SimpleFieldSetBuilder();
+
+               soneBuilder.put(prefix + "Name", sone.getName());
+               soneBuilder.put(prefix + "NiceName", SoneAccessor.getNiceName(sone));
+               soneBuilder.put(prefix + "LastUpdated", sone.getTime());
+               if (localSone != null) {
+                       soneBuilder.put(prefix + "Followed", String.valueOf(localSone.hasFriend(sone.getId())));
+               }
+               Profile profile = sone.getProfile();
+               soneBuilder.put(prefix + "Field.Count", profile.getFields().size());
+               int fieldIndex = 0;
+               for (Field field : profile.getFields()) {
+                       soneBuilder.put(prefix + "Field." + fieldIndex + ".Name", field.getName());
+                       soneBuilder.put(prefix + "Field." + fieldIndex + ".Value", field.getValue());
+                       ++fieldIndex;
+               }
+
+               return soneBuilder.get();
+       }
+
+       /**
+        * Creates a simple field set from the given collection of Sones.
+        *
+        * @param sones
+        *            The Sones to encode
+        * @param prefix
+        *            The prefix for the field names (may be empty but not
+        *            {@code null})
+        * @return The simple field set containing the given Sones
+        */
+       protected SimpleFieldSet encodeSones(Collection<? extends Sone> sones, String prefix) {
+               SimpleFieldSetBuilder soneBuilder = new SimpleFieldSetBuilder();
+
+               int soneIndex = 0;
+               soneBuilder.put(prefix + "Count", sones.size());
+               for (Sone sone : sones) {
+                       String sonePrefix = prefix + soneIndex++ + ".";
+                       soneBuilder.put(sonePrefix + "ID", sone.getId());
+                       soneBuilder.put(sonePrefix + "Name", sone.getName());
+                       soneBuilder.put(sonePrefix + "NiceName", SoneAccessor.getNiceName(sone));
+                       soneBuilder.put(sonePrefix + "Time", sone.getTime());
+               }
+
+               return soneBuilder.get();
+       }
+
+       /**
+        * Creates a simple field set from the given post.
+        *
+        * @param post
+        *            The post to encode
+        * @param prefix
+        *            The prefix for the field names (may be empty but not
+        *            {@code null})
+        * @param includeReplies
+        *            {@code true} to include replies, {@code false} to not include
+        *            replies
+        * @return The simple field set containing the post
+        */
+       protected SimpleFieldSet encodePost(Post post, String prefix, boolean includeReplies) {
+               SimpleFieldSetBuilder postBuilder = new SimpleFieldSetBuilder();
+
+               postBuilder.put(prefix + "ID", post.getId());
+               postBuilder.put(prefix + "Sone", post.getSone().getId());
+               if (post.getRecipient() != null) {
+                       postBuilder.put(prefix + "Recipient", post.getRecipient().getId());
+               }
+               postBuilder.put(prefix + "Time", post.getTime());
+               postBuilder.put(prefix + "Text", encodeString(post.getText()));
+               postBuilder.put(encodeLikes(core.getLikes(post), prefix + "Likes."));
+
+               if (includeReplies) {
+                       List<Reply> replies = core.getReplies(post);
+                       postBuilder.put(encodeReplies(replies, prefix));
+               }
+
+               return postBuilder.get();
+       }
+
+       /**
+        * Creates a simple field set from the given collection of posts.
+        *
+        * @param posts
+        *            The posts to encode
+        * @param prefix
+        *            The prefix for the field names (may be empty but not
+        *            {@code null})
+        * @param includeReplies
+        *            {@code true} to include the replies, {@code false} to not
+        *            include the replies
+        * @return The simple field set containing the posts
+        */
+       protected SimpleFieldSet encodePosts(Collection<? extends Post> posts, String prefix, boolean includeReplies) {
+               SimpleFieldSetBuilder postBuilder = new SimpleFieldSetBuilder();
+
+               int postIndex = 0;
+               postBuilder.put(prefix + "Count", posts.size());
+               for (Post post : posts) {
+                       String postPrefix = prefix + postIndex++;
+                       postBuilder.put(encodePost(post, postPrefix + ".", includeReplies));
+                       if (includeReplies) {
+                               postBuilder.put(encodeReplies(Filters.filteredList(core.getReplies(post), Reply.FUTURE_REPLIES_FILTER), postPrefix + "."));
+                       }
+               }
+
+               return postBuilder.get();
+       }
+
+       /**
+        * Creates a simple field set from the given collection of replies.
+        *
+        * @param replies
+        *            The replies to encode
+        * @param prefix
+        *            The prefix for the field names (may be empty, but not
+        *            {@code null})
+        * @return The simple field set containing the replies
+        */
+       protected SimpleFieldSet encodeReplies(Collection<? extends Reply> replies, String prefix) {
+               SimpleFieldSetBuilder replyBuilder = new SimpleFieldSetBuilder();
+
+               int replyIndex = 0;
+               replyBuilder.put(prefix + "Replies.Count", replies.size());
+               for (Reply reply : replies) {
+                       String replyPrefix = prefix + "Replies." + replyIndex++ + ".";
+                       replyBuilder.put(replyPrefix + "ID", reply.getId());
+                       replyBuilder.put(replyPrefix + "Sone", reply.getSone().getId());
+                       replyBuilder.put(replyPrefix + "Time", reply.getTime());
+                       replyBuilder.put(replyPrefix + "Text", encodeString(reply.getText()));
+               }
+
+               return replyBuilder.get();
+       }
+
+       /**
+        * Creates a simple field set from the given collection of Sones that like
+        * an element.
+        *
+        * @param likes
+        *            The liking Sones
+        * @param prefix
+        *            The prefix for the field names (may be empty but not
+        *            {@code null})
+        * @return The simple field set containing the likes
+        */
+       protected SimpleFieldSet encodeLikes(Collection<? extends Sone> likes, String prefix) {
+               SimpleFieldSetBuilder likesBuilder = new SimpleFieldSetBuilder();
+
+               int likeIndex = 0;
+               likesBuilder.put(prefix + "Count", likes.size());
+               for (Sone sone : likes) {
+                       String sonePrefix = prefix + likeIndex++ + ".";
+                       likesBuilder.put(sonePrefix + "ID", sone.getId());
+               }
+
+               return likesBuilder.get();
+       }
+
+       //
+       // OBJECT METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public String toString() {
+               return getClass().getName() + "[writeAccess=" + writeAccess + "]";
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/fcp/CreatePostCommand.java b/src/main/java/net/pterodactylus/sone/fcp/CreatePostCommand.java
new file mode 100644 (file)
index 0000000..4dd0b8e
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+ * Sone - CreatePostCommand.java - Copyright © 2011 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.fcp;
+
+import net.pterodactylus.sone.core.Core;
+import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.freenet.SimpleFieldSetBuilder;
+import net.pterodactylus.sone.freenet.fcp.FcpException;
+import freenet.support.SimpleFieldSet;
+import freenet.support.api.Bucket;
+
+/**
+ * FCP command that creates a new {@link Post}.
+ *
+ * @see Core#createPost(Sone, Sone, String)
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class CreatePostCommand extends AbstractSoneCommand {
+
+       /**
+        * Creates a new “CreatePost” FCP command.
+        *
+        * @param core
+        *            The Sone core
+        */
+       public CreatePostCommand(Core core) {
+               super(core, true);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public Response execute(SimpleFieldSet parameters, Bucket data, AccessType accessType) throws FcpException {
+               Sone sone = getSone(parameters, "Sone", true);
+               String text = getString(parameters, "Text");
+               Sone recipient = null;
+               if (parameters.get("Recipient") != null) {
+                       recipient = getSone(parameters, "Recipient", false);
+               }
+               if (sone.equals(recipient)) {
+                       return new ErrorResponse("Sone and Recipient must not be the same.");
+               }
+               Post post = getCore().createPost(sone, recipient, text);
+               return new Response("PostCreated", new SimpleFieldSetBuilder().put("Post", post.getId()).get());
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/fcp/CreateReplyCommand.java b/src/main/java/net/pterodactylus/sone/fcp/CreateReplyCommand.java
new file mode 100644 (file)
index 0000000..880d51e
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ * Sone - CreateReplyCommand.java - Copyright © 2011 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.fcp;
+
+import net.pterodactylus.sone.core.Core;
+import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.sone.data.Reply;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.freenet.SimpleFieldSetBuilder;
+import net.pterodactylus.sone.freenet.fcp.FcpException;
+import freenet.support.SimpleFieldSet;
+import freenet.support.api.Bucket;
+
+/**
+ * FCP command that creates a new {@link Reply}.
+ *
+ * @see Core#createReply(Sone, Post, String)
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class CreateReplyCommand extends AbstractSoneCommand {
+
+       /**
+        * Creates a new “CreateReply” FCP command.
+        *
+        * @param core
+        *            The Sone core
+        */
+       public CreateReplyCommand(Core core) {
+               super(core, true);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public Response execute(SimpleFieldSet parameters, Bucket data, AccessType accessType) throws FcpException {
+               Sone sone = getSone(parameters, "Sone", true);
+               Post post = getPost(parameters, "Post");
+               String text = getString(parameters, "Text");
+               Reply reply = getCore().createReply(sone, post, text);
+               return new Response("ReplyCreated", new SimpleFieldSetBuilder().put("Reply", reply.getId()).get());
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/fcp/DeletePostCommand.java b/src/main/java/net/pterodactylus/sone/fcp/DeletePostCommand.java
new file mode 100644 (file)
index 0000000..3c8b03a
--- /dev/null
@@ -0,0 +1,57 @@
+/*
+ * Sone - DeletePostCommand.java - Copyright © 2011 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.fcp;
+
+import net.pterodactylus.sone.core.Core;
+import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.sone.freenet.SimpleFieldSetBuilder;
+import net.pterodactylus.sone.freenet.fcp.FcpException;
+import freenet.support.SimpleFieldSet;
+import freenet.support.api.Bucket;
+
+/**
+ * FCP command that deletes a {@link Post}.
+ *
+ * @see Core#deletePost(Post)
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class DeletePostCommand extends AbstractSoneCommand {
+
+       /**
+        * Creates a new “DeletePost” FCP command.
+        *
+        * @param core
+        *            The Sone core
+        */
+       public DeletePostCommand(Core core) {
+               super(core, true);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public Response execute(SimpleFieldSet parameters, Bucket data, AccessType accessType) throws FcpException {
+               Post post = getPost(parameters, "Post");
+               if (!getCore().isLocalSone(post.getSone())) {
+                       return new ErrorResponse(401, "Not allowed.");
+               }
+               return new Response("PostDeleted", new SimpleFieldSetBuilder().get());
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/fcp/DeleteReplyCommand.java b/src/main/java/net/pterodactylus/sone/fcp/DeleteReplyCommand.java
new file mode 100644 (file)
index 0000000..4ac9c15
--- /dev/null
@@ -0,0 +1,57 @@
+/*
+ * Sone - DeleteReplyCommand.java - Copyright © 2011 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.fcp;
+
+import net.pterodactylus.sone.core.Core;
+import net.pterodactylus.sone.data.Reply;
+import net.pterodactylus.sone.freenet.SimpleFieldSetBuilder;
+import net.pterodactylus.sone.freenet.fcp.FcpException;
+import freenet.support.SimpleFieldSet;
+import freenet.support.api.Bucket;
+
+/**
+ * FCP command that deletes a {@link Reply}.
+ *
+ * @see Core#deleteReply(Reply)
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class DeleteReplyCommand extends AbstractSoneCommand {
+
+       /**
+        * Creates a new “DeleteReply” FCP command.
+        *
+        * @param core
+        *            The Sone core
+        */
+       public DeleteReplyCommand(Core core) {
+               super(core, true);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public Response execute(SimpleFieldSet parameters, Bucket data, AccessType accessType) throws FcpException {
+               Reply reply = getReply(parameters, "Reply");
+               if (!getCore().isLocalSone(reply.getSone())) {
+                       return new ErrorResponse(401, "Not allowed.");
+               }
+               return new Response("ReplyDeleted", new SimpleFieldSetBuilder().get());
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/fcp/FcpInterface.java b/src/main/java/net/pterodactylus/sone/fcp/FcpInterface.java
new file mode 100644 (file)
index 0000000..65d0d87
--- /dev/null
@@ -0,0 +1,214 @@
+/*
+ * Sone - FcpInterface.java - Copyright © 2011 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.fcp;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import net.pterodactylus.sone.core.Core;
+import net.pterodactylus.sone.freenet.fcp.Command.AccessType;
+import net.pterodactylus.sone.freenet.fcp.Command.ErrorResponse;
+import net.pterodactylus.sone.freenet.fcp.Command.Response;
+import net.pterodactylus.sone.freenet.fcp.FcpException;
+import net.pterodactylus.util.logging.Logging;
+import net.pterodactylus.util.validation.Validation;
+import freenet.pluginmanager.FredPluginFCP;
+import freenet.pluginmanager.PluginNotFoundException;
+import freenet.pluginmanager.PluginReplySender;
+import freenet.support.SimpleFieldSet;
+import freenet.support.api.Bucket;
+
+/**
+ * Implementation of an FCP interface for other clients or plugins to
+ * communicate with Sone.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class FcpInterface {
+
+       /**
+        * The action level that full access for the FCP connection is required.
+        *
+        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+        */
+       public enum FullAccessRequired {
+
+               /** No action requires full access. */
+               NO,
+
+               /** All writing actions require full access. */
+               WRITING,
+
+               /** All actions require full access. */
+               ALWAYS,
+
+       }
+
+       /** The logger. */
+       private static final Logger logger = Logging.getLogger(FcpInterface.class);
+
+       /** Whether the FCP interface is currently active. */
+       private volatile boolean active;
+
+       /** What function full access is required for. */
+       private volatile FullAccessRequired fullAccessRequired = FullAccessRequired.ALWAYS;
+
+       /** All available FCP commands. */
+       private final Map<String, AbstractSoneCommand> commands = Collections.synchronizedMap(new HashMap<String, AbstractSoneCommand>());
+
+       /**
+        * Creates a new FCP interface.
+        *
+        * @param core
+        *            The core
+        */
+       public FcpInterface(Core core) {
+               commands.put("Version", new VersionCommand(core));
+               commands.put("GetLocalSones", new GetLocalSonesCommand(core));
+               commands.put("GetSones", new GetSonesCommand(core));
+               commands.put("GetSone", new GetSoneCommand(core));
+               commands.put("GetPost", new GetPostCommand(core));
+               commands.put("GetPosts", new GetPostsCommand(core));
+               commands.put("GetPostFeed", new GetPostFeedCommand(core));
+               commands.put("LikePost", new LikePostCommand(core));
+               commands.put("LikeReply", new LikeReplyCommand(core));
+               commands.put("CreatePost", new CreatePostCommand(core));
+               commands.put("CreateReply", new CreateReplyCommand(core));
+               commands.put("DeletePost", new DeletePostCommand(core));
+               commands.put("DeleteReply", new DeleteReplyCommand(core));
+       }
+
+       //
+       // ACCESSORS
+       //
+
+       /**
+        * Sets whether the FCP interface should handle requests. If {@code active}
+        * is {@code false}, all requests are answered with an error.
+        *
+        * @param active
+        *            {@code true} to activate the FCP interface, {@code false} to
+        *            deactivate the FCP interface
+        */
+       public void setActive(boolean active) {
+               this.active = active;
+       }
+
+       /**
+        * Sets the action level for which full FCP access is required.
+        *
+        * @param fullAccessRequired
+        *            The action level for which full FCP access is required
+        */
+       public void setFullAccessRequired(FullAccessRequired fullAccessRequired) {
+               Validation.begin().isNotNull("FullAccessRequired", fullAccessRequired).check();
+               this.fullAccessRequired = fullAccessRequired;
+       }
+
+       //
+       // ACTIONS
+       //
+
+       /**
+        * Handles a plugin FCP request.
+        *
+        * @param pluginReplySender
+        *            The reply sender
+        * @param parameters
+        *            The message parameters
+        * @param data
+        *            The message data (may be {@code null})
+        * @param accessType
+        *            One of {@link FredPluginFCP#ACCESS_DIRECT},
+        *            {@link FredPluginFCP#ACCESS_FCP_FULL},
+        *            {@link FredPluginFCP#ACCESS_FCP_RESTRICTED}
+        */
+       public void handle(PluginReplySender pluginReplySender, SimpleFieldSet parameters, Bucket data, int accessType) {
+               if (!active) {
+                       try {
+                               sendReply(pluginReplySender, null, new ErrorResponse(400, "FCP Interface deactivated"));
+                       } catch (PluginNotFoundException pnfe1) {
+                               logger.log(Level.FINE, "Could not set error to plugin.", pnfe1);
+                       }
+                       return;
+               }
+               AbstractSoneCommand command = commands.get(parameters.get("Message"));
+               if ((accessType == FredPluginFCP.ACCESS_FCP_RESTRICTED) && (((fullAccessRequired == FullAccessRequired.WRITING) && command.requiresWriteAccess()) || (fullAccessRequired == FullAccessRequired.ALWAYS))) {
+                       try {
+                               sendReply(pluginReplySender, null, new ErrorResponse(401, "Not authorized"));
+                       } catch (PluginNotFoundException pnfe1) {
+                               logger.log(Level.FINE, "Could not set error to plugin.", pnfe1);
+                       }
+                       return;
+               }
+               try {
+                       if (command == null) {
+                               sendReply(pluginReplySender, null, new ErrorResponse("Unrecognized Message: " + parameters.get("Message")));
+                               return;
+                       }
+                       String identifier = parameters.get("Identifier");
+                       if ((identifier == null) || (identifier.length() == 0)) {
+                               sendReply(pluginReplySender, null, new ErrorResponse("Missing Identifier."));
+                               return;
+                       }
+                       try {
+                               Response response = command.execute(parameters, data, AccessType.values()[accessType]);
+                               sendReply(pluginReplySender, identifier, response);
+                       } catch (FcpException fe1) {
+                               logger.log(Level.WARNING, "Could not process FCP command “%s”.", command);
+                               sendReply(pluginReplySender, identifier, new ErrorResponse("Error executing command: " + fe1.getMessage()));
+                       }
+               } catch (PluginNotFoundException pnfe1) {
+                       logger.log(Level.WARNING, "Could not find destination plugin: " + pluginReplySender);
+               }
+       }
+
+       //
+       // PRIVATE METHODS
+       //
+
+       /**
+        * Sends the given response to the given plugin.
+        *
+        * @param pluginReplySender
+        *            The reply sender
+        * @param identifier
+        *            The identifier (may be {@code null})
+        * @param response
+        *            The response to send
+        * @throws PluginNotFoundException
+        *             if the plugin can not be found
+        */
+       private void sendReply(PluginReplySender pluginReplySender, String identifier, Response response) throws PluginNotFoundException {
+               SimpleFieldSet replyParameters = response.getReplyParameters();
+               if (identifier != null) {
+                       replyParameters.putOverwrite("Identifier", identifier);
+               }
+               if (response.hasData()) {
+                       pluginReplySender.send(replyParameters, response.getData());
+               } else if (response.hasBucket()) {
+                       pluginReplySender.send(replyParameters, response.getBucket());
+               } else {
+                       pluginReplySender.send(replyParameters);
+               }
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/fcp/GetLocalSonesCommand.java b/src/main/java/net/pterodactylus/sone/fcp/GetLocalSonesCommand.java
new file mode 100644 (file)
index 0000000..c4206d5
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+ * Sone - GetLocalSonesCommand.java - Copyright © 2011 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.fcp;
+
+import net.pterodactylus.sone.core.Core;
+import net.pterodactylus.sone.freenet.fcp.FcpException;
+import freenet.support.SimpleFieldSet;
+import freenet.support.api.Bucket;
+
+/**
+ * Implements the “GetLocalSones” FCP command that returns the list of local
+ * Sones to the sender.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class GetLocalSonesCommand extends AbstractSoneCommand {
+
+       /**
+        * Creates a new “GetLocalSones” FCP command.
+        *
+        * @param core
+        *            The Sone core
+        */
+       public GetLocalSonesCommand(Core core) {
+               super(core);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public Response execute(SimpleFieldSet parameters, Bucket data, AccessType accessType) throws FcpException {
+               return new Response("ListLocalSones", encodeSones(getCore().getLocalSones(), "LocalSones."));
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/fcp/GetPostCommand.java b/src/main/java/net/pterodactylus/sone/fcp/GetPostCommand.java
new file mode 100644 (file)
index 0000000..bb87407
--- /dev/null
@@ -0,0 +1,54 @@
+/*
+ * Sone - GetPostCommand.java - Copyright © 2011 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.fcp;
+
+import net.pterodactylus.sone.core.Core;
+import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.sone.freenet.fcp.FcpException;
+import freenet.support.SimpleFieldSet;
+import freenet.support.api.Bucket;
+
+/**
+ * The “GetPost” FCP command returns a single {@link Post} to an FCP client.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class GetPostCommand extends AbstractSoneCommand {
+
+       /**
+        * Creates a new “GetPost” FCP command.
+        *
+        * @param core
+        *            The Sone core
+        */
+       public GetPostCommand(Core core) {
+               super(core);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public Response execute(SimpleFieldSet parameters, Bucket data, AccessType accessType) throws FcpException {
+               Post post = getPost(parameters, "Post");
+               boolean includeReplies = getBoolean(parameters, "IncludeReplies", true);
+
+               return new Response("Post", encodePost(post, "Post.", includeReplies));
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/fcp/GetPostFeedCommand.java b/src/main/java/net/pterodactylus/sone/fcp/GetPostFeedCommand.java
new file mode 100644 (file)
index 0000000..d4b175c
--- /dev/null
@@ -0,0 +1,82 @@
+/*
+ * Sone - GetPostFeedCommand.java - Copyright © 2011 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.fcp;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import net.pterodactylus.sone.core.Core;
+import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.freenet.fcp.FcpException;
+import net.pterodactylus.util.filter.Filters;
+import freenet.support.SimpleFieldSet;
+import freenet.support.api.Bucket;
+
+/**
+ * Implementation of an FCP interface for other clients or plugins to
+ * communicate with Sone.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class GetPostFeedCommand extends AbstractSoneCommand {
+
+       /**
+        * Creates a new “GetPostFeed” command.
+        *
+        * @param core
+        *            The core
+        */
+       public GetPostFeedCommand(Core core) {
+               super(core);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public Response execute(SimpleFieldSet parameters, Bucket data, AccessType accessType) throws FcpException {
+               Sone sone = getSone(parameters, "Sone", true);
+               int startPost = getInt(parameters, "StartPost", 0);
+               int maxPosts = getInt(parameters, "MaxPosts", -1);
+
+               Set<Post> allPosts = new HashSet<Post>();
+               allPosts.addAll(sone.getPosts());
+               for (String friendSoneId : sone.getFriends()) {
+                       if (!getCore().hasSone(friendSoneId)) {
+                               continue;
+                       }
+                       allPosts.addAll(getCore().getSone(friendSoneId).getPosts());
+               }
+               allPosts.addAll(getCore().getDirectedPosts(sone));
+               allPosts = Filters.filteredSet(allPosts, Post.FUTURE_POSTS_FILTER);
+
+               List<Post> sortedPosts = new ArrayList<Post>(allPosts);
+               Collections.sort(sortedPosts, Post.TIME_COMPARATOR);
+
+               if (sortedPosts.size() < startPost) {
+                       return new Response("PostFeed", encodePosts(Collections.<Post> emptyList(), "Posts.", false));
+               }
+
+               return new Response("PostFeed", encodePosts(sortedPosts.subList(startPost, (maxPosts == -1) ? sortedPosts.size() : Math.min(startPost + maxPosts, sortedPosts.size())), "Posts.", true));
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/fcp/GetPostsCommand.java b/src/main/java/net/pterodactylus/sone/fcp/GetPostsCommand.java
new file mode 100644 (file)
index 0000000..6a3b07e
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * Sone - GetPostsCommand.java - Copyright © 2011 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.fcp;
+
+import java.util.Collections;
+import java.util.List;
+
+import net.pterodactylus.sone.core.Core;
+import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.freenet.fcp.FcpException;
+import freenet.support.SimpleFieldSet;
+import freenet.support.api.Bucket;
+
+/**
+ * Implements the “GetPosts” FCP command that returns the list of posts a Sone
+ * made.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class GetPostsCommand extends AbstractSoneCommand {
+
+       /**
+        * Creates a new “GetPosts” FCP command.
+        *
+        * @param core
+        *            The Sone core
+        */
+       public GetPostsCommand(Core core) {
+               super(core);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public Response execute(SimpleFieldSet parameters, Bucket data, AccessType accessType) throws FcpException {
+               Sone sone = getSone(parameters, "Sone", false);
+               int startPost = getInt(parameters, "StartPost", 0);
+               int maxPosts = getInt(parameters, "MaxPosts", -1);
+               List<Post> posts = sone.getPosts();
+               if (posts.size() < startPost) {
+                       return new Response("Posts", encodePosts(Collections.<Post> emptyList(), "Posts.", false));
+               }
+               return new Response("Posts", encodePosts(sone.getPosts().subList(startPost, (maxPosts == -1) ? posts.size() : Math.min(startPost + maxPosts, posts.size())), "Posts.", true));
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/fcp/GetSoneCommand.java b/src/main/java/net/pterodactylus/sone/fcp/GetSoneCommand.java
new file mode 100644 (file)
index 0000000..5ebdc13
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * Sone - GetSoneCommand.java - Copyright © 2011 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.fcp;
+
+import net.pterodactylus.sone.core.Core;
+import net.pterodactylus.sone.data.Profile;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.freenet.fcp.FcpException;
+import freenet.support.SimpleFieldSet;
+import freenet.support.api.Bucket;
+
+/**
+ * Implements the “GetSone“ FCP command which returns {@link Profile}
+ * information about a {@link Sone}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class GetSoneCommand extends AbstractSoneCommand {
+
+       /**
+        * Creates a new “GetSone” FCP command.
+        *
+        * @param core
+        *            The Sone core
+        */
+       protected GetSoneCommand(Core core) {
+               super(core);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public Response execute(SimpleFieldSet parameters, Bucket data, AccessType accessType) throws FcpException {
+               Sone sone = getSone(parameters, "Sone", false);
+               Sone localSone = getSone(parameters, "LocalSone", false, false);
+               return new Response("Sone", encodeSone(sone, "", localSone));
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/fcp/GetSonesCommand.java b/src/main/java/net/pterodactylus/sone/fcp/GetSonesCommand.java
new file mode 100644 (file)
index 0000000..1729bb7
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+ * Sone - GetSonesCommand.java - Copyright © 2011 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.fcp;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import net.pterodactylus.sone.core.Core;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.freenet.fcp.FcpException;
+import freenet.support.SimpleFieldSet;
+import freenet.support.api.Bucket;
+
+/**
+ * Implements the “GetSones” FCP command that returns the list of known Sones.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class GetSonesCommand extends AbstractSoneCommand {
+
+       /**
+        * Creates a new “GetSones” FCP command.
+        *
+        * @param core
+        *            The Sone core
+        */
+       public GetSonesCommand(Core core) {
+               super(core);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public Response execute(SimpleFieldSet parameters, Bucket data, AccessType accessType) throws FcpException {
+               int startSone = getInt(parameters, "StartSone", 0);
+               int maxSones = getInt(parameters, "MaxSones", -1);
+               List<Sone> sones = new ArrayList<Sone>(getCore().getSones());
+               if (sones.size() < startSone) {
+                       return new Response("Sones", encodeSones(Collections.<Sone> emptyList(), ""));
+               }
+               Collections.sort(sones, Sone.NICE_NAME_COMPARATOR);
+               return new Response("Sones", encodeSones(sones.subList(startSone, (maxSones == -1) ? sones.size() : Math.min(startSone + maxSones, sones.size())), ""));
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/fcp/LikePostCommand.java b/src/main/java/net/pterodactylus/sone/fcp/LikePostCommand.java
new file mode 100644 (file)
index 0000000..37f69ec
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+ * Sone - LikePostCommand.java - Copyright © 2011 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.fcp;
+
+import net.pterodactylus.sone.core.Core;
+import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.freenet.SimpleFieldSetBuilder;
+import net.pterodactylus.sone.freenet.fcp.FcpException;
+import freenet.support.SimpleFieldSet;
+import freenet.support.api.Bucket;
+
+/**
+ * Implements the “LikePost” FCP command which allows the user to like a post.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class LikePostCommand extends AbstractSoneCommand {
+
+       /**
+        * Creates a new “LikePost” FCP command.
+        *
+        * @param core
+        *            The Sone core
+        */
+       protected LikePostCommand(Core core) {
+               super(core, true);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public Response execute(SimpleFieldSet parameters, Bucket data, AccessType accessType) throws FcpException {
+               Post post = getPost(parameters, "Post");
+               Sone sone = getSone(parameters, "Sone", true);
+               sone.addLikedPostId(post.getId());
+               return new Response("PostLiked", new SimpleFieldSetBuilder().put("LikeCount", getCore().getLikes(post).size()).get());
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/fcp/LikeReplyCommand.java b/src/main/java/net/pterodactylus/sone/fcp/LikeReplyCommand.java
new file mode 100644 (file)
index 0000000..2c8f04f
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+ * Sone - LikeReplyCommand.java - Copyright © 2011 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.fcp;
+
+import net.pterodactylus.sone.core.Core;
+import net.pterodactylus.sone.data.Reply;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.freenet.SimpleFieldSetBuilder;
+import net.pterodactylus.sone.freenet.fcp.FcpException;
+import freenet.support.SimpleFieldSet;
+import freenet.support.api.Bucket;
+
+/**
+ * Implements the “LikeReply” FCP command which allows the user to like a reply.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class LikeReplyCommand extends AbstractSoneCommand {
+
+       /**
+        * Creates a new “LikeReply” FCP command.
+        *
+        * @param core
+        *            The Sone core
+        */
+       protected LikeReplyCommand(Core core) {
+               super(core, true);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public Response execute(SimpleFieldSet parameters, Bucket data, AccessType accessType) throws FcpException {
+               Reply reply = getReply(parameters, "Reply");
+               Sone sone = getSone(parameters, "Sone", true);
+               sone.addLikedReplyId(reply.getId());
+               return new Response("ReplyLiked", new SimpleFieldSetBuilder().put("LikeCount", getCore().getLikes(reply).size()).get());
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/fcp/VersionCommand.java b/src/main/java/net/pterodactylus/sone/fcp/VersionCommand.java
new file mode 100644 (file)
index 0000000..1e135b5
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+ * Sone - VersionCommand.java - Copyright © 2011 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.fcp;
+
+import net.pterodactylus.sone.core.Core;
+import net.pterodactylus.sone.freenet.SimpleFieldSetBuilder;
+import net.pterodactylus.sone.main.SonePlugin;
+import freenet.support.SimpleFieldSet;
+import freenet.support.api.Bucket;
+
+/**
+ * Returns version information about the Sone plugin.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class VersionCommand extends AbstractSoneCommand {
+
+       /**
+        * Creates a new “Version” FCP command.
+        *
+        * @param core
+        *            The Sone core
+        */
+       protected VersionCommand(Core core) {
+               super(core);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public Response execute(SimpleFieldSet parameters, Bucket data, AccessType accessType) {
+               return new Response("Version", new SimpleFieldSetBuilder().put("Version", SonePlugin.VERSION.toString()).put("ProtocolVersion", 1).get());
+       }
+
+}
index ef1a183..910e0d6 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * FreenetSone - L10nFilter.java - Copyright © 2010 David Roden
+ * Sone - L10nFilter.java - Copyright © 2010 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
 
 package net.pterodactylus.sone.freenet;
 
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 
 import net.pterodactylus.util.template.Filter;
@@ -49,7 +53,21 @@ public class L10nFilter implements Filter {
         */
        @Override
        public String format(TemplateContext templateContext, Object data, Map<String, String> parameters) {
-               return l10n.getString(String.valueOf(data));
+               if (parameters.isEmpty()) {
+                       return l10n.getString(String.valueOf(data));
+               }
+               List<Object> parameterValues = new ArrayList<Object>();
+               int parameterIndex = 0;
+               while (parameters.containsKey(String.valueOf(parameterIndex))) {
+                       Object value = parameters.get(String.valueOf(parameterIndex));
+                       if (((String) value).startsWith("=")) {
+                               value = ((String) value).substring(1);
+                       } else {
+                               value = templateContext.get((String) value);
+                       }
+                       parameterValues.add(value);
+                       ++parameterIndex;
+               }
+               return new MessageFormat(l10n.getString(String.valueOf(data)), new Locale(l10n.getSelectedLanguage().shortCode)).format(parameterValues.toArray());
        }
-
 }
index 5e30de1..5aa68cf 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * FreenetSone - PluginStoreConfigurationBackend.java - Copyright © 2010 David Roden
+ * Sone - PluginStoreConfigurationBackend.java - Copyright © 2010 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
diff --git a/src/main/java/net/pterodactylus/sone/freenet/SimpleFieldSetBuilder.java b/src/main/java/net/pterodactylus/sone/freenet/SimpleFieldSetBuilder.java
new file mode 100644 (file)
index 0000000..37d049a
--- /dev/null
@@ -0,0 +1,120 @@
+/*
+ * Sone - SimpleFieldSetBuilder.java - Copyright © 2011 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.freenet;
+
+import net.pterodactylus.util.validation.Validation;
+import freenet.support.SimpleFieldSet;
+
+/**
+ * Helper class to construct {@link SimpleFieldSet} objects in a single call.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class SimpleFieldSetBuilder {
+
+       /** The simple field set that is being constructed. */
+       private final SimpleFieldSet simpleFieldSet;
+
+       /**
+        * Creates a new simple field set builder using a new, empty simple field
+        * set.
+        */
+       public SimpleFieldSetBuilder() {
+               this(new SimpleFieldSet(true));
+       }
+
+       /**
+        * Creates a new simple field set builder that will return the given simple
+        * field set on {@link #get()}.
+        *
+        * @param simpleFieldSet
+        *            The simple field set to build
+        */
+       public SimpleFieldSetBuilder(SimpleFieldSet simpleFieldSet) {
+               Validation.begin().isNotNull("Simple Field Set", simpleFieldSet).check();
+               this.simpleFieldSet = simpleFieldSet;
+       }
+
+       /**
+        * Returns the constructed simple field set.
+        *
+        * @return The construct simple field set
+        */
+       public SimpleFieldSet get() {
+               return simpleFieldSet;
+       }
+
+       /**
+        * Copies the given simple field set into the simple field set being built
+        * in this builder, overwriting all previously existing values.
+        *
+        * @param simpleFieldSet
+        *            The simple field set to copy
+        * @return This simple field set builder
+        */
+       public SimpleFieldSetBuilder put(@SuppressWarnings("hiding") SimpleFieldSet simpleFieldSet) {
+               this.simpleFieldSet.putAllOverwrite(simpleFieldSet);
+               return this;
+       }
+
+       /**
+        * Stores the given value under the given key, overwriting any previous
+        * value.
+        *
+        * @param key
+        *            The key of the value
+        * @param value
+        *            The value to store
+        * @return This simple field set builder
+        */
+       public SimpleFieldSetBuilder put(String key, String value) {
+               simpleFieldSet.putOverwrite(key, value);
+               return this;
+       }
+
+       /**
+        * Stores the given value under the given key, overwriting any previous
+        * value.
+        *
+        * @param key
+        *            The key of the value
+        * @param value
+        *            The value to store
+        * @return This simple field set builder
+        */
+       public SimpleFieldSetBuilder put(String key, int value) {
+               simpleFieldSet.put(key, value);
+               return this;
+       }
+
+       /**
+        * Stores the given value under the given key, overwriting any previous
+        * value.
+        *
+        * @param key
+        *            The key of the value
+        * @param value
+        *            The value to store
+        * @return This simple field set builder
+        */
+       public SimpleFieldSetBuilder put(String key, long value) {
+               simpleFieldSet.put(key, value);
+               return this;
+       }
+
+}
index ebe036a..2993b2f 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * FreenetSone - TemplateBucket.java - Copyright © 2010 David Roden
+ * Sone - StringBucket.java - Copyright © 2010 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
diff --git a/src/main/java/net/pterodactylus/sone/freenet/fcp/AbstractCommand.java b/src/main/java/net/pterodactylus/sone/freenet/fcp/AbstractCommand.java
new file mode 100644 (file)
index 0000000..538876a
--- /dev/null
@@ -0,0 +1,127 @@
+/*
+ * Sone - AbstractCommand.java - Copyright © 2011 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.freenet.fcp;
+
+import freenet.node.FSParseException;
+import freenet.support.SimpleFieldSet;
+
+/**
+ * Basic implementation of a {@link Command} with various helper methods to
+ * simplify processing of input parameters.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public abstract class AbstractCommand implements Command {
+
+       //
+       // PROTECTED METHODS
+       //
+
+       /**
+        * Returns a String value from the given simple field set.
+        *
+        * @param simpleFieldSet
+        *            The simple field set to get the value from
+        * @param key
+        *            The key of the value
+        * @return The String value
+        * @throws FcpException
+        *             if there is no value for the given key in the simple field
+        *             set, or the value can not be converted to a String
+        */
+       protected String getString(SimpleFieldSet simpleFieldSet, String key) throws FcpException {
+               try {
+                       return simpleFieldSet.getString(key);
+               } catch (FSParseException fspe1) {
+                       throw new FcpException("Could not get parameter “" + key + "” as String.", fspe1);
+               }
+       }
+
+       /**
+        * Returns an int value from the given simple field set.
+        *
+        * @param simpleFieldSet
+        *            The simple field set to get the value from
+        * @param key
+        *            The key of the value
+        * @return The int value
+        * @throws FcpException
+        *             if there is no value for the given key in the simple field
+        *             set, or the value can not be converted to an int
+        */
+       protected int getInt(SimpleFieldSet simpleFieldSet, String key) throws FcpException {
+               try {
+                       return simpleFieldSet.getInt(key);
+               } catch (FSParseException fspe1) {
+                       throw new FcpException("Could not get parameter “" + key + "” as int.", fspe1);
+               }
+       }
+
+       /**
+        * Returns an int value from the given simple field set, returning a default
+        * value if the value can not be found or converted.
+        *
+        * @param simpleFieldSet
+        *            The simple field set to get the value from
+        * @param key
+        *            The key of the value
+        * @param defaultValue
+        *            The default value
+        * @return The int value
+        */
+       protected int getInt(SimpleFieldSet simpleFieldSet, String key, int defaultValue) {
+               return simpleFieldSet.getInt(key, defaultValue);
+       }
+
+       /**
+        * Returns a boolean value from the given simple field set.
+        *
+        * @param simpleFieldSet
+        *            The simple field set to get the value from
+        * @param key
+        *            The key of the value
+        * @return The boolean value
+        * @throws FcpException
+        *             if there is no value for the given key in the simple field
+        *             set, or the value can not be converted to a boolean
+        */
+       protected boolean getBoolean(SimpleFieldSet simpleFieldSet, String key) throws FcpException {
+               try {
+                       return simpleFieldSet.getBoolean(key);
+               } catch (FSParseException fspe1) {
+                       throw new FcpException("Could not get parameter “" + key + "” as boolean.", fspe1);
+               }
+       }
+
+       /**
+        * Returns a boolean value from the given simple field set, returning a
+        * default value if the value can not be found or converted.
+        *
+        * @param simpleFieldSet
+        *            The simple field set to get the value from
+        * @param key
+        *            The key of the value
+        * @param defaultValue
+        *            The default value
+        * @return The boolean value
+        */
+       protected boolean getBoolean(SimpleFieldSet simpleFieldSet, String key, boolean defaultValue) {
+               return simpleFieldSet.getBoolean(key, defaultValue);
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/freenet/fcp/Command.java b/src/main/java/net/pterodactylus/sone/freenet/fcp/Command.java
new file mode 100644 (file)
index 0000000..46032dd
--- /dev/null
@@ -0,0 +1,226 @@
+/*
+ * Sone - Command.java - Copyright © 2011 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.freenet.fcp;
+
+import net.pterodactylus.sone.freenet.SimpleFieldSetBuilder;
+import freenet.support.SimpleFieldSet;
+import freenet.support.api.Bucket;
+
+/**
+ * Implementation of an FCP interface for other clients or plugins to
+ * communicate with Sone.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface Command {
+
+       /**
+        * Executes the command, returning a reply that will be sent back to the
+        * requesting plugin.
+        *
+        * @param parameters
+        *            The parameters of the comand
+        * @param data
+        *            The data of the command (may be {@code null})
+        * @param accessType
+        *            The access type
+        * @return A reply to send back to the plugin
+        * @throws FcpException
+        *             if an error processing the parameters occurs
+        */
+       public Response execute(SimpleFieldSet parameters, Bucket data, AccessType accessType) throws FcpException;
+
+       /**
+        * The access type of the request.
+        *
+        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+        */
+       public static enum AccessType {
+
+               /** Access from another plugin. */
+               DIRECT,
+
+               /** Access via restricted FCP. */
+               RESTRICTED_FCP,
+
+               /** Access via FCP with full access. */
+               FULL_FCP,
+
+       }
+
+       /**
+        * Interface for command replies.
+        *
+        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+        */
+       public static class Response {
+
+               /** The message name of the reponse. */
+               private final String messageName;
+
+               /** The reply parameters. */
+               private final SimpleFieldSet replyParameters;
+
+               /** The reply data, may be {@code null}. */
+               private final byte[] data;
+
+               /** The data bucket, may be {@code null}. */
+               private final Bucket bucket;
+
+               /**
+                * Creates a new reply with the given parameters.
+                *
+                * @param messageName
+                *            The message name
+                * @param replyParameters
+                *            The reply parameters
+                */
+               public Response(String messageName, SimpleFieldSet replyParameters) {
+                       this(messageName, replyParameters, null, null);
+               }
+
+               /**
+                * Creates a new reply with the given parameters.
+                *
+                * @param messageName
+                *            The message name
+                * @param replyParameters
+                *            The reply parameters
+                * @param data
+                *            The data of the reply (may be {@code null})
+                */
+               public Response(String messageName, SimpleFieldSet replyParameters, byte[] data) {
+                       this(messageName, replyParameters, data, null);
+               }
+
+               /**
+                * Creates a new reply with the given parameters.
+                *
+                * @param messageName
+                *            The message name
+                * @param replyParameters
+                *            The reply parameters
+                * @param bucket
+                *            The bucket of the reply (may be {@code null})
+                */
+               public Response(String messageName, SimpleFieldSet replyParameters, Bucket bucket) {
+                       this(messageName, replyParameters, null, bucket);
+               }
+
+               /**
+                * Creates a new reply with the given parameters.
+                *
+                * @param messageName
+                *            The message name
+                * @param replyParameters
+                *            The reply parameters
+                * @param data
+                *            The data of the reply (may be {@code null})
+                * @param bucket
+                *            The bucket of the reply (may be {@code null})
+                */
+               private Response(String messageName, SimpleFieldSet replyParameters, byte[] data, Bucket bucket) {
+                       this.messageName = messageName;
+                       this.replyParameters = replyParameters;
+                       this.data = data;
+                       this.bucket = bucket;
+               }
+
+               /**
+                * Returns the reply parameters.
+                *
+                * @return The reply parameters
+                */
+               public SimpleFieldSet getReplyParameters() {
+                       return new SimpleFieldSetBuilder(replyParameters).put("Message", messageName).get();
+               }
+
+               /**
+                * Returns whether the reply has reply data.
+                *
+                * @see #getData()
+                * @return {@code true} if this reply has data, {@code false} otherwise
+                */
+               public boolean hasData() {
+                       return data != null;
+               }
+
+               /**
+                * Returns the data of the reply.
+                *
+                * @return The data of the reply
+                */
+               public byte[] getData() {
+                       return data;
+               }
+
+               /**
+                * Returns whether the reply has a data bucket.
+                *
+                * @see #getBucket()
+                * @return {@code true} if the reply has a data bucket, {@code false}
+                *         otherwise
+                */
+               public boolean hasBucket() {
+                       return bucket != null;
+               }
+
+               /**
+                * Returns the data bucket of the reply.
+                *
+                * @return The data bucket of the reply
+                */
+               public Bucket getBucket() {
+                       return bucket;
+               }
+
+       }
+
+       /**
+        * Response implementation that can return an error message and an optional
+        * error code.
+        *
+        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+        */
+       public class ErrorResponse extends Response {
+
+               /**
+                * Creates a new error response with the given message.
+                *
+                * @param message
+                *            The error message
+                */
+               public ErrorResponse(String message) {
+                       super("Error", new SimpleFieldSetBuilder().put("ErrorMessage", message).get());
+               }
+
+               /**
+                * Creates a new error response with the given code and message.
+                *
+                * @param code
+                *            The error code
+                * @param message
+                *            The error message
+                */
+               public ErrorResponse(int code, String message) {
+                       super("Error", new SimpleFieldSetBuilder().put("ErrorMessage", message).put("ErrorCode", code).get());
+               }
+
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/freenet/fcp/FcpException.java b/src/main/java/net/pterodactylus/sone/freenet/fcp/FcpException.java
new file mode 100644 (file)
index 0000000..d194840
--- /dev/null
@@ -0,0 +1,66 @@
+/*
+ * Sone - FcpException.java - Copyright © 2011 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.freenet.fcp;
+
+/**
+ * Base exception for FCP communication.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class FcpException extends Exception {
+
+       /**
+        * Creates a new FCP exception.
+        */
+       public FcpException() {
+               super();
+       }
+
+       /**
+        * Creates a new FCP exception.
+        *
+        * @param message
+        *            The message of the exception
+        */
+       public FcpException(String message) {
+               super(message);
+       }
+
+       /**
+        * Creates a new FCP exception.
+        *
+        * @param cause
+        *            The cause of the exception
+        */
+       public FcpException(Throwable cause) {
+               super(cause);
+       }
+
+       /**
+        * Creates a new FCP exception.
+        *
+        * @param message
+        *            The message of the exception
+        * @param cause
+        *            The cause of the exception
+        */
+       public FcpException(String message, Throwable cause) {
+               super(message, cause);
+       }
+
+}
index 71710e4..3da08f3 100644 (file)
@@ -19,7 +19,6 @@ package net.pterodactylus.sone.freenet.plugin;
 
 import java.util.EventListener;
 
-
 import freenet.support.SimpleFieldSet;
 import freenet.support.api.Bucket;
 
index 3c04823..46f81d6 100644 (file)
@@ -64,6 +64,7 @@ public class DefaultIdentity implements Identity {
        private final Map<String, String> properties = Collections.synchronizedMap(new HashMap<String, String>());
 
        /** Cached trust. */
+       /* synchronize on itself. */
        private final WritableCache<OwnIdentity, Trust> trustCache = new MemoryCache<OwnIdentity, Trust>(new ValueRetriever<OwnIdentity, Trust>() {
 
                @Override
@@ -249,7 +250,9 @@ public class DefaultIdentity implements Identity {
        @Override
        public Trust getTrust(OwnIdentity ownIdentity) {
                try {
-                       return trustCache.get(ownIdentity);
+                       synchronized (trustCache) {
+                               return trustCache.get(ownIdentity);
+                       }
                } catch (CacheException ce1) {
                        logger.log(Level.WARNING, "Could not get trust for OwnIdentity: " + ownIdentity, ce1);
                        return null;
@@ -265,7 +268,9 @@ public class DefaultIdentity implements Identity {
         *            The trust received for this identity
         */
        void setTrustPrivate(OwnIdentity ownIdentity, Trust trust) {
-               trustCache.put(ownIdentity, trust);
+               synchronized (trustCache) {
+                       trustCache.put(ownIdentity, trust);
+               }
        }
 
        //
index 98ba2c7..803bc32 100644 (file)
@@ -178,23 +178,21 @@ public class IdentityManager extends AbstractService {
                Map<OwnIdentity, Map<String, Identity>> oldIdentities = Collections.emptyMap();
                while (!shouldStop()) {
                        Map<OwnIdentity, Map<String, Identity>> currentIdentities = new HashMap<OwnIdentity, Map<String, Identity>>();
+                       @SuppressWarnings("hiding")
                        Map<String, OwnIdentity> currentOwnIdentities = new HashMap<String, OwnIdentity>();
 
+                       Set<OwnIdentity> ownIdentities = null;
+                       boolean identitiesLoaded = false;
                        try {
                                /* get all identities with the wanted context from WoT. */
-                               Set<OwnIdentity> ownIdentities = webOfTrustConnector.loadAllOwnIdentities();
+                               ownIdentities = webOfTrustConnector.loadAllOwnIdentities();
 
-                               /* check for changes. */
-                               for (OwnIdentity ownIdentity : ownIdentities) {
-                                       currentOwnIdentities.put(ownIdentity.getId(), ownIdentity);
-                               }
-                               checkOwnIdentities(currentOwnIdentities);
-
-                               /* now filter for context and get all identities. */
+                               /* load trusted identities. */
                                for (OwnIdentity ownIdentity : ownIdentities) {
                                        if ((context != null) && !ownIdentity.hasContext(context)) {
                                                continue;
                                        }
+                                       currentOwnIdentities.put(ownIdentity.getId(), ownIdentity);
 
                                        Set<Identity> trustedIdentities = webOfTrustConnector.loadTrustedIdentities(ownIdentity, context);
                                        Map<String, Identity> identities = new HashMap<String, Identity>();
@@ -202,6 +200,19 @@ public class IdentityManager extends AbstractService {
                                        for (Identity identity : trustedIdentities) {
                                                identities.put(identity.getId(), identity);
                                        }
+                               }
+                               identitiesLoaded = true;
+                       } catch (WebOfTrustException wote1) {
+                               logger.log(Level.WARNING, "WoT has disappeared!", wote1);
+                       }
+
+                       if (identitiesLoaded) {
+
+                               /* check for changes. */
+                               checkOwnIdentities(currentOwnIdentities);
+
+                               /* now check for changes in remote identities. */
+                               for (OwnIdentity ownIdentity : currentOwnIdentities.values()) {
 
                                        /* find new identities. */
                                        for (Identity currentIdentity : currentIdentities.get(ownIdentity).values()) {
@@ -258,13 +269,10 @@ public class IdentityManager extends AbstractService {
                                                        }
                                                }
                                        }
-
-                                       /* remember the current set of identities. */
-                                       oldIdentities = currentIdentities;
                                }
 
-                       } catch (WebOfTrustException wote1) {
-                               logger.log(Level.WARNING, "WoT has disappeared!", wote1);
+                               /* remember the current set of identities. */
+                               oldIdentities = currentIdentities;
                        }
 
                        /* wait a minute before checking again. */
index 59da039..b4ea53d 100644 (file)
@@ -77,6 +77,7 @@ public class WebOfTrustConnector implements ConnectorListener {
         *             if the own identities can not be loaded
         */
        public Set<OwnIdentity> loadAllOwnIdentities() throws WebOfTrustException {
+               @SuppressWarnings("hiding")
                Reply reply = performRequest(SimpleFieldSetConstructor.create().put("Message", "GetOwnIdentities").get());
                SimpleFieldSet fields = reply.getFields();
                int ownIdentityCounter = -1;
@@ -124,6 +125,7 @@ public class WebOfTrustConnector implements ConnectorListener {
         *             if an error occured talking to the Web of Trust plugin
         */
        public Set<Identity> loadTrustedIdentities(OwnIdentity ownIdentity, String context) throws PluginException {
+               @SuppressWarnings("hiding")
                Reply reply = performRequest(SimpleFieldSetConstructor.create().put("Message", "GetIdentitiesByScore").put("TreeOwner", ownIdentity.getId()).put("Selection", "+").put("Context", (context == null) ? "" : context).get());
                SimpleFieldSet fields = reply.getFields();
                Set<Identity> identities = new HashSet<Identity>();
@@ -183,6 +185,7 @@ public class WebOfTrustConnector implements ConnectorListener {
         *             if an error occured talking to the Web of Trust plugin
         */
        public String getProperty(Identity identity, String name) throws PluginException {
+               @SuppressWarnings("hiding")
                Reply reply = performRequest(SimpleFieldSetConstructor.create().put("Message", "GetProperty").put("Identity", identity.getId()).put("Property", name).get());
                return reply.getFields().get("Property");
        }
@@ -396,7 +399,7 @@ public class WebOfTrustConnector implements ConnectorListener {
         * {@inheritDoc}
         */
        @Override
-       public void receivedReply(PluginConnector pluginConnector, SimpleFieldSet fields, Bucket data) {
+       public void receivedReply(@SuppressWarnings("hiding") PluginConnector pluginConnector, SimpleFieldSet fields, Bucket data) {
                String messageName = fields.get("Message");
                logger.log(Level.FINEST, "Received Reply from Plugin: " + messageName);
                synchronized (reply) {
index de4fee8..91fb547 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * FreenetSone - SonePlugin.java - Copyright © 2010 David Roden
+ * Sone - SonePlugin.java - Copyright © 2010 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
@@ -24,6 +24,7 @@ import java.util.logging.Logger;
 
 import net.pterodactylus.sone.core.Core;
 import net.pterodactylus.sone.core.FreenetInterface;
+import net.pterodactylus.sone.fcp.FcpInterface;
 import net.pterodactylus.sone.freenet.PluginStoreConfigurationBackend;
 import net.pterodactylus.sone.freenet.plugin.PluginConnector;
 import net.pterodactylus.sone.freenet.wot.IdentityManager;
@@ -40,10 +41,14 @@ import freenet.l10n.BaseL10n.LANGUAGE;
 import freenet.l10n.PluginL10n;
 import freenet.pluginmanager.FredPlugin;
 import freenet.pluginmanager.FredPluginBaseL10n;
+import freenet.pluginmanager.FredPluginFCP;
 import freenet.pluginmanager.FredPluginL10n;
 import freenet.pluginmanager.FredPluginThreadless;
 import freenet.pluginmanager.FredPluginVersioned;
+import freenet.pluginmanager.PluginReplySender;
 import freenet.pluginmanager.PluginRespirator;
+import freenet.support.SimpleFieldSet;
+import freenet.support.api.Bucket;
 
 /**
  * This class interfaces with Freenet. It is the class that is loaded by the
@@ -51,7 +56,7 @@ import freenet.pluginmanager.PluginRespirator;
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
-public class SonePlugin implements FredPlugin, FredPluginL10n, FredPluginBaseL10n, FredPluginThreadless, FredPluginVersioned {
+public class SonePlugin implements FredPlugin, FredPluginFCP, FredPluginL10n, FredPluginBaseL10n, FredPluginThreadless, FredPluginVersioned {
 
        static {
                /* initialize logging. */
@@ -78,7 +83,7 @@ public class SonePlugin implements FredPlugin, FredPluginL10n, FredPluginBaseL10
        }
 
        /** The version. */
-       public static final Version VERSION = new Version(0, 6, 1);
+       public static final Version VERSION = new Version(0, 6, 7);
 
        /** The logger. */
        private static final Logger logger = Logging.getLogger(SonePlugin.class);
@@ -92,6 +97,9 @@ public class SonePlugin implements FredPlugin, FredPluginL10n, FredPluginBaseL10
        /** The web interface. */
        private WebInterface webInterface;
 
+       /** The FCP interface. */
+       private FcpInterface fcpInterface;
+
        /** The l10n helper. */
        private PluginL10n l10n;
 
@@ -137,7 +145,7 @@ public class SonePlugin implements FredPlugin, FredPluginL10n, FredPluginBaseL10
         * {@inheritDoc}
         */
        @Override
-       public void runPlugin(PluginRespirator pluginRespirator) {
+       public void runPlugin(@SuppressWarnings("hiding") PluginRespirator pluginRespirator) {
                this.pluginRespirator = pluginRespirator;
 
                /* create a configuration. */
@@ -184,6 +192,10 @@ public class SonePlugin implements FredPlugin, FredPluginL10n, FredPluginBaseL10
                        webInterface = new WebInterface(this);
                        core.addCoreListener(webInterface);
 
+                       /* create FCP interface. */
+                       fcpInterface = new FcpInterface(core);
+                       core.setFcpInterface(fcpInterface);
+
                        /* create the identity manager. */
                        identityManager.addIdentityListener(core);
 
@@ -233,6 +245,18 @@ public class SonePlugin implements FredPlugin, FredPluginL10n, FredPluginBaseL10
        }
 
        //
+       // INTERFACE FredPluginFCP
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public void handle(PluginReplySender pluginReplySender, SimpleFieldSet parameters, Bucket data, int accessType) {
+               fcpInterface.handle(pluginReplySender, parameters, data, accessType);
+       }
+
+       //
        // INTERFACE FredPluginL10n
        //
 
index c66d376..74e3100 100644 (file)
@@ -101,7 +101,8 @@ public class ListNotification<T> extends TemplateNotification {
        }
 
        /**
-        * Sets the elements to show in this notification.
+        * Sets the elements to show in this notification. This method will not call
+        * {@link #touch()}.
         *
         * @param elements
         *            The elements to show
index a6ef8aa..3db8912 100644 (file)
@@ -24,7 +24,10 @@ import java.util.List;
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Reply;
 import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.freenet.wot.OwnIdentity;
+import net.pterodactylus.sone.freenet.wot.Trust;
 import net.pterodactylus.util.notify.Notification;
+import net.pterodactylus.util.validation.Validation;
 
 /**
  * Filter for {@link ListNotification}s.
@@ -47,50 +50,56 @@ public class ListNotificationFilters {
         *            The current Sone, or {@code null} if not logged in
         * @return The filtered notifications
         */
-       public static List<Notification> filterNotifications(List<Notification> notifications, Sone currentSone) {
-               ListNotification<Post> newPostNotification = getNotification(notifications, "new-post-notification", Post.class);
-               if (newPostNotification != null) {
-                       ListNotification<Post> filteredNotification = filterNewPostNotification(newPostNotification, currentSone);
-                       int notificationIndex = notifications.indexOf(newPostNotification);
-                       if (filteredNotification == null) {
-                               notifications.remove(notificationIndex);
-                       } else {
-                               notifications.set(notificationIndex, filteredNotification);
-                       }
-               }
-               ListNotification<Reply> newReplyNotification = getNotification(notifications, "new-replies-notification", Reply.class);
-               if (newReplyNotification != null) {
-                       ListNotification<Reply> filteredNotification = filterNewReplyNotification(newReplyNotification, currentSone);
-                       int notificationIndex = notifications.indexOf(newReplyNotification);
-                       if (filteredNotification == null) {
-                               notifications.remove(notificationIndex);
+       @SuppressWarnings("unchecked")
+       public static List<Notification> filterNotifications(Collection<? extends Notification> notifications, Sone currentSone) {
+               List<Notification> filteredNotifications = new ArrayList<Notification>();
+               for (Notification notification : notifications) {
+                       if (notification.getId().equals("new-post-notification")) {
+                               ListNotification<Post> filteredNotification = filterNewPostNotification((ListNotification<Post>) notification, currentSone, true);
+                               if (filteredNotification != null) {
+                                       filteredNotifications.add(filteredNotification);
+                               }
+                       } else if (notification.getId().equals("new-reply-notification")) {
+                               ListNotification<Reply> filteredNotification = filterNewReplyNotification((ListNotification<Reply>) notification, currentSone);
+                               if (filteredNotification != null) {
+                                       filteredNotifications.add(filteredNotification);
+                               }
+                       } else if (notification.getId().equals("mention-notification")) {
+                               ListNotification<Post> filteredNotification = filterNewPostNotification((ListNotification<Post>) notification, null, false);
+                               if (filteredNotification != null) {
+                                       filteredNotifications.add(filteredNotification);
+                               }
                        } else {
-                               notifications.set(notificationIndex, filteredNotification);
+                               filteredNotifications.add(notification);
                        }
                }
-               return notifications;
+               return filteredNotifications;
        }
 
        /**
         * Filters the new posts of the given notification. If {@code currentSone}
-        * is {@code null}, {@code null} is returned and the notification is
-        * subsequently removed. Otherwise only posts that are posted by friend
-        * Sones of the given Sone are retained; all other posts are removed.
+        * is {@code null} and {@code soneRequired} is {@code true}, {@code null} is
+        * returned and the notification is subsequently removed. Otherwise only
+        * posts that are posted by friend Sones of the given Sone are retained; all
+        * other posts are removed.
         *
         * @param newPostNotification
         *            The new-post notification
         * @param currentSone
         *            The current Sone, or {@code null} if not logged in
+        * @param soneRequired
+        *            Whether a non-{@code null} Sone in {@code currentSone} is
+        *            required
         * @return The filtered new-post notification, or {@code null} if the
         *         notification should be removed
         */
-       private static ListNotification<Post> filterNewPostNotification(ListNotification<Post> newPostNotification, Sone currentSone) {
-               if (currentSone == null) {
+       public static ListNotification<Post> filterNewPostNotification(ListNotification<Post> newPostNotification, Sone currentSone, boolean soneRequired) {
+               if (soneRequired && (currentSone == null)) {
                        return null;
                }
                List<Post> newPosts = new ArrayList<Post>();
                for (Post post : newPostNotification.getElements()) {
-                       if (currentSone.hasFriend(post.getSone().getId()) || currentSone.equals(post.getSone()) || currentSone.equals(post.getRecipient())) {
+                       if (isPostVisible(currentSone, post)) {
                                newPosts.add(post);
                        }
                }
@@ -102,6 +111,7 @@ public class ListNotificationFilters {
                }
                ListNotification<Post> filteredNotification = new ListNotification<Post>(newPostNotification);
                filteredNotification.setElements(newPosts);
+               filteredNotification.setLastUpdateTime(newPostNotification.getLastUpdatedTime());
                return filteredNotification;
        }
 
@@ -119,13 +129,13 @@ public class ListNotificationFilters {
         * @return The filtered new-reply notification, or {@code null} if the
         *         notification should be removed
         */
-       private static ListNotification<Reply> filterNewReplyNotification(ListNotification<Reply> newReplyNotification, Sone currentSone) {
+       public static ListNotification<Reply> filterNewReplyNotification(ListNotification<Reply> newReplyNotification, Sone currentSone) {
                if (currentSone == null) {
                        return null;
                }
                List<Reply> newReplies = new ArrayList<Reply>();
                for (Reply reply : newReplyNotification.getElements()) {
-                       if (((reply.getPost().getSone() != null) && currentSone.hasFriend(reply.getPost().getSone().getId())) || currentSone.equals(reply.getPost().getSone()) || currentSone.equals(reply.getPost().getRecipient())) {
+                       if (isReplyVisible(currentSone, reply)) {
                                newReplies.add(reply);
                        }
                }
@@ -137,33 +147,109 @@ public class ListNotificationFilters {
                }
                ListNotification<Reply> filteredNotification = new ListNotification<Reply>(newReplyNotification);
                filteredNotification.setElements(newReplies);
+               filteredNotification.setLastUpdateTime(newReplyNotification.getLastUpdatedTime());
                return filteredNotification;
        }
 
        /**
-        * Finds the notification with the given ID in the list of notifications and
-        * returns it.
+        * Checks whether a post is visible to the given Sone. A post is not
+        * considered visible if one of the following statements is true:
+        * <ul>
+        * <li>The post does not have a Sone.</li>
+        * <li>The post’s {@link Post#getTime() time} is in the future.</li>
+        * </ul>
+        * <p>
+        * If {@code post} is not {@code null} more checks are performed, and the
+        * post will be invisible if:
+        * </p>
+        * <ul>
+        * <li>The Sone of the post is not the given Sone, the given Sone does not
+        * follow the post’s Sone, and the given Sone is not the recipient of the
+        * post.</li>
+        * <li>The trust relationship between the two Sones can not be retrieved.</li>
+        * <li>The given Sone has explicitely assigned negative trust to the post’s
+        * Sone.</li>
+        * <li>The given Sone has not explicitely assigned negative trust to the
+        * post’s Sone but the implicit trust is negative.</li>
+        * </ul>
+        * If none of these statements is true the post is considered visible.
         *
-        * @param <T>
-        *            The type of the item in the notification
-        * @param notifications
-        *            The notification to search
-        * @param notificationId
-        *            The ID of the requested notification
-        * @param notificationElementClass
-        *            The class of the notification item
-        * @return The requested notification, or {@code null} if no notification
-        *         with the given ID could be found
+        * @param sone
+        *            The Sone that checks for a post’s visibility (may be
+        *            {@code null} to skip Sone-specific checks, such as trust)
+        * @param post
+        *            The post to check for visibility
+        * @return {@code true} if the post is considered visible, {@code false}
+        *         otherwise
         */
-       @SuppressWarnings("unchecked")
-       private static <T> ListNotification<T> getNotification(Collection<? extends Notification> notifications, String notificationId, Class<T> notificationElementClass) {
-               for (Notification notification : notifications) {
-                       if (!notificationId.equals(notification.getId())) {
-                               continue;
+       public static boolean isPostVisible(Sone sone, Post post) {
+               Validation.begin().isNotNull("Post", post).check();
+               Sone postSone = post.getSone();
+               if (postSone == null) {
+                       return false;
+               }
+               if (sone != null) {
+                       Trust trust = postSone.getIdentity().getTrust((OwnIdentity) sone.getIdentity());
+                       if (trust != null) {
+                               if ((trust.getExplicit() != null) && (trust.getExplicit() < 0)) {
+                                       return false;
+                               }
+                               if ((trust.getExplicit() == null) && (trust.getImplicit() != null) && (trust.getImplicit() < 0)) {
+                                       return false;
+                               }
+                       } else {
+                               return false;
+                       }
+                       if ((!postSone.equals(sone)) && !sone.hasFriend(postSone.getId()) && !sone.equals(post.getRecipient())) {
+                               return false;
                        }
-                       return (ListNotification<T>) notification;
                }
-               return null;
+               if (post.getTime() > System.currentTimeMillis()) {
+                       return false;
+               }
+               return true;
+       }
+
+       /**
+        * Checks whether a reply is visible to the given Sone. A reply is not
+        * considered visible if one of the following statements is true:
+        * <ul>
+        * <li>The reply does not have a post.</li>
+        * <li>The reply’s post does not have a Sone.</li>
+        * <li>The Sone of the reply’s post is not the given Sone, the given Sone
+        * does not follow the reply’s post’s Sone, and the given Sone is not the
+        * recipient of the reply’s post.</li>
+        * <li>The trust relationship between the two Sones can not be retrieved.</li>
+        * <li>The given Sone has explicitely assigned negative trust to the post’s
+        * Sone.</li>
+        * <li>The given Sone has not explicitely assigned negative trust to the
+        * reply’s post’s Sone but the implicit trust is negative.</li>
+        * <li>The reply’s post’s {@link Post#getTime() time} is in the future.</li>
+        * <li>The reply’s {@link Reply#getTime() time} is in the future.</li>
+        * </ul>
+        * If none of these statements is true the reply is considered visible.
+        *
+        * @param sone
+        *            The Sone that checks for a post’s visibility (may be
+        *            {@code null} to skip Sone-specific checks, such as trust)
+        * @param reply
+        *            The reply to check for visibility
+        * @return {@code true} if the reply is considered visible, {@code false}
+        *         otherwise
+        */
+       public static boolean isReplyVisible(Sone sone, Reply reply) {
+               Validation.begin().isNotNull("Reply", reply).check();
+               Post post = reply.getPost();
+               if (post == null) {
+                       return false;
+               }
+               if (!isPostVisible(sone, post)) {
+                       return false;
+               }
+               if (reply.getTime() > System.currentTimeMillis()) {
+                       return false;
+               }
+               return true;
        }
 
 }
index a25d619..b2a4962 100644 (file)
@@ -19,9 +19,10 @@ package net.pterodactylus.sone.template;
 
 import java.util.Map;
 
-import net.pterodactylus.sone.web.page.Page.Request;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Plugin;
 import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Request;
 
 /**
  * Extracts a page number from a {@link Request}’s parameters and stores it in
@@ -50,7 +51,7 @@ public class GetPagePlugin implements Plugin {
                        pageKey = "page";
                }
 
-               Request request = (Request) templateContext.get(requestKey);
+               FreenetRequest request = (FreenetRequest) templateContext.get(requestKey);
                String pageString = request.getHttpRequest().getParam(parameter);
                int page = 0;
                try {
diff --git a/src/main/java/net/pterodactylus/sone/template/NotificationManagerAccessor.java b/src/main/java/net/pterodactylus/sone/template/NotificationManagerAccessor.java
deleted file mode 100644 (file)
index c4468ea..0000000
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Sone - NotificationManagerAccessor.java - Copyright © 2010 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.template;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.notify.ListNotificationFilters;
-import net.pterodactylus.util.notify.Notification;
-import net.pterodactylus.util.notify.NotificationManager;
-import net.pterodactylus.util.template.ReflectionAccessor;
-import net.pterodactylus.util.template.TemplateContext;
-
-/**
- * Adds additional properties to a {@link NotificationManager}.
- * <dl>
- * <dd>all</dd>
- * <dt>Returns all notifications, sorted by creation time, oldest first.</dt>
- * <dd>new</dd>
- * <dt>Returns all changed notifications, sorted by last updated time, newest
- * first.</dt>
- * </dl>
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class NotificationManagerAccessor extends ReflectionAccessor {
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public Object get(TemplateContext templateContext, Object object, String member) {
-               NotificationManager notificationManager = (NotificationManager) object;
-               if ("all".equals(member)) {
-                       List<Notification> notifications = ListNotificationFilters.filterNotifications(new ArrayList<Notification>(notificationManager.getNotifications()), (Sone) templateContext.get("currentSone"));
-                       Collections.sort(notifications, Notification.CREATED_TIME_SORTER);
-                       return notifications;
-               }
-               return super.get(templateContext, object, member);
-       }
-
-}
index 477e972..3494c21 100644 (file)
@@ -19,18 +19,31 @@ package net.pterodactylus.sone.template;
 
 import java.io.IOException;
 import java.io.StringReader;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Map;
 
 import net.pterodactylus.sone.core.Core;
 import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.text.FreenetLinkParser;
-import net.pterodactylus.sone.text.FreenetLinkParserContext;
+import net.pterodactylus.sone.text.FreenetLinkPart;
+import net.pterodactylus.sone.text.LinkPart;
+import net.pterodactylus.sone.text.Part;
+import net.pterodactylus.sone.text.PlainTextPart;
+import net.pterodactylus.sone.text.PostPart;
+import net.pterodactylus.sone.text.SonePart;
+import net.pterodactylus.sone.text.SoneTextParser;
+import net.pterodactylus.sone.text.SoneTextParserContext;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Filter;
+import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
 import net.pterodactylus.util.template.TemplateContextFactory;
+import net.pterodactylus.util.template.TemplateParser;
 
 /**
- * Filter that filters a given text through a {@link FreenetLinkParser}.
+ * Filter that filters a given text through a {@link SoneTextParser}.
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
@@ -40,20 +53,32 @@ public class ParserFilter implements Filter {
        private final Core core;
 
        /** The link parser. */
-       private final FreenetLinkParser linkParser;
+       private final SoneTextParser soneTextParser;
+
+       /** The template context factory. */
+       private final TemplateContextFactory templateContextFactory;
+
+       /** The template for {@link PlainTextPart}s. */
+       private final Template plainTextTemplate = TemplateParser.parse(new StringReader("<%text|html>"));
+
+       /** The template for {@link FreenetLinkPart}s. */
+       private final Template linkTemplate = TemplateParser.parse(new StringReader("<a class=\"<%cssClass|html>\" href=\"<%link|html>\" title=\"<%title|html>\"><%text|html></a>"));
 
        /**
-        * Creates a new filter that runs its input through a
-        * {@link FreenetLinkParser}.
+        * Creates a new filter that runs its input through a {@link SoneTextParser}
+        * .
         *
         * @param core
         *            The core
         * @param templateContextFactory
         *            The context factory for rendering the parts
+        * @param soneTextParser
+        *            The Sone text parser
         */
-       public ParserFilter(Core core, TemplateContextFactory templateContextFactory) {
+       public ParserFilter(Core core, TemplateContextFactory templateContextFactory, SoneTextParser soneTextParser) {
                this.core = core;
-               linkParser = new FreenetLinkParser(core, templateContextFactory);
+               this.templateContextFactory = templateContextFactory;
+               this.soneTextParser = soneTextParser;
        }
 
        /**
@@ -62,6 +87,19 @@ public class ParserFilter implements Filter {
        @Override
        public Object format(TemplateContext templateContext, Object data, Map<String, String> parameters) {
                String text = String.valueOf(data);
+               int length = -1;
+               try {
+                       length = Integer.parseInt(parameters.get("length"));
+               } catch (NumberFormatException nfe1) {
+                       /* ignore. */
+               }
+               if ((length == -1) && (parameters.get("length") != null)) {
+                       try {
+                               length = Integer.parseInt(String.valueOf(templateContext.get(parameters.get("length"))));
+                       } catch (NumberFormatException nfe1) {
+                               /* ignore. */
+                       }
+               }
                String soneKey = parameters.get("sone");
                if (soneKey == null) {
                        soneKey = "sone";
@@ -70,13 +108,189 @@ public class ParserFilter implements Filter {
                if (sone == null) {
                        sone = core.getSone(soneKey, false);
                }
-               FreenetLinkParserContext context = new FreenetLinkParserContext(sone);
+               FreenetRequest request = (FreenetRequest) templateContext.get("request");
+               SoneTextParserContext context = new SoneTextParserContext(request, sone);
+               StringWriter parsedTextWriter = new StringWriter();
                try {
-                       return linkParser.parse(context, new StringReader(text));
+                       Iterable<Part> parts = soneTextParser.parse(context, new StringReader(text));
+                       if (length > -1) {
+                               List<Part> shortenedParts = new ArrayList<Part>();
+                               for (Part part : parts) {
+                                       if (part instanceof PlainTextPart) {
+                                               String longText = ((PlainTextPart) part).getText();
+                                               if (length >= longText.length()) {
+                                                       shortenedParts.add(part);
+                                               } else {
+                                                       shortenedParts.add(new PlainTextPart(longText.substring(0, length) + "…"));
+                                               }
+                                               length -= longText.length();
+                                       } else if (part instanceof LinkPart) {
+                                               shortenedParts.add(part);
+                                               length -= ((LinkPart) part).getText().length();
+                                       } else {
+                                               shortenedParts.add(part);
+                                       }
+                                       if (length <= 0) {
+                                               break;
+                                       }
+                               }
+                               parts = shortenedParts;
+                       }
+                       render(parsedTextWriter, parts);
                } catch (IOException ioe1) {
-                       /* no exceptions in a StringReader, ignore. */
+                       /* no exceptions in a StringReader or StringWriter, ignore. */
+               }
+               return parsedTextWriter.toString();
+       }
+
+       //
+       // PRIVATE METHODS
+       //
+
+       /**
+        * Renders the given parts.
+        *
+        * @param writer
+        *            The writer to render the parts to
+        * @param parts
+        *            The parts to render
+        */
+       private void render(Writer writer, Iterable<Part> parts) {
+               for (Part part : parts) {
+                       render(writer, part);
+               }
+       }
+
+       /**
+        * Renders the given part.
+        *
+        * @param writer
+        *            The writer to render the part to
+        * @param part
+        *            The part to render
+        */
+       @SuppressWarnings("unchecked")
+       private void render(Writer writer, Part part) {
+               if (part instanceof PlainTextPart) {
+                       render(writer, (PlainTextPart) part);
+               } else if (part instanceof FreenetLinkPart) {
+                       render(writer, (FreenetLinkPart) part);
+               } else if (part instanceof LinkPart) {
+                       render(writer, (LinkPart) part);
+               } else if (part instanceof SonePart) {
+                       render(writer, (SonePart) part);
+               } else if (part instanceof PostPart) {
+                       render(writer, (PostPart) part);
+               } else if (part instanceof Iterable<?>) {
+                       render(writer, (Iterable<Part>) part);
+               }
+       }
+
+       /**
+        * Renders the given plain-text part.
+        *
+        * @param writer
+        *            The writer to render the part to
+        * @param plainTextPart
+        *            The part to render
+        */
+       private void render(Writer writer, PlainTextPart plainTextPart) {
+               TemplateContext templateContext = templateContextFactory.createTemplateContext();
+               templateContext.set("text", plainTextPart.getText());
+               plainTextTemplate.render(templateContext, writer);
+       }
+
+       /**
+        * Renders the given freenet link part.
+        *
+        * @param writer
+        *            The writer to render the part to
+        * @param freenetLinkPart
+        *            The part to render
+        */
+       private void render(Writer writer, FreenetLinkPart freenetLinkPart) {
+               renderLink(writer, "/" + freenetLinkPart.getLink(), freenetLinkPart.getText(), freenetLinkPart.getTitle(), freenetLinkPart.isTrusted() ? "freenet-trusted" : "freenet");
+       }
+
+       /**
+        * Renders the given link part.
+        *
+        * @param writer
+        *            The writer to render the part to
+        * @param linkPart
+        *            The part to render
+        */
+       private void render(Writer writer, LinkPart linkPart) {
+               renderLink(writer, "/?_CHECKED_HTTP_=" + linkPart.getLink(), linkPart.getText(), linkPart.getTitle(), "internet");
+       }
+
+       /**
+        * Renders the given Sone part.
+        *
+        * @param writer
+        *            The writer to render the part to
+        * @param sonePart
+        *            The part to render
+        */
+       private void render(Writer writer, SonePart sonePart) {
+               renderLink(writer, "viewSone.html?sone=" + sonePart.getSone().getId(), SoneAccessor.getNiceName(sonePart.getSone()), SoneAccessor.getNiceName(sonePart.getSone()), "in-sone");
+       }
+
+       /**
+        * Renders the given post part.
+        *
+        * @param writer
+        *            The writer to render the part to
+        * @param postPart
+        *            The part to render
+        */
+       private void render(Writer writer, PostPart postPart) {
+               renderLink(writer, "viewPost.html?post=" + postPart.getPost().getId(), getExcerpt(postPart.getPost().getText(), 20), SoneAccessor.getNiceName(postPart.getPost().getSone()), "in-sone");
+       }
+
+       /**
+        * Renders the given link.
+        *
+        * @param writer
+        *            The writer to render the link to
+        * @param link
+        *            The link to render
+        * @param text
+        *            The text of the link
+        * @param title
+        *            The title of the link
+        * @param cssClass
+        *            The CSS class of the link
+        */
+       private void renderLink(Writer writer, String link, String text, String title, String cssClass) {
+               TemplateContext templateContext = templateContextFactory.createTemplateContext();
+               templateContext.set("cssClass", cssClass);
+               templateContext.set("link", link);
+               templateContext.set("text", text);
+               templateContext.set("title", title);
+               linkTemplate.render(templateContext, writer);
+       }
+
+       //
+       // STATIC METHODS
+       //
+
+       /**
+        * Returns up to {@code length} characters from the given text, appending
+        * “…” if the text is longer.
+        *
+        * @param text
+        *            The text to get an excerpt from
+        * @param length
+        *            The maximum length of the excerpt (without the ellipsis)
+        * @return The excerpt of the text
+        */
+       private static String getExcerpt(String text, int length) {
+               String filteredText = text.replaceAll("(\r\n)+", "\r\n").replaceAll("\n+", "\n").replace("\r\n", " ").replace('\n', ' ');
+               if (filteredText.length() > length) {
+                       return filteredText.substring(0, length) + "…";
                }
-               return null;
+               return filteredText;
        }
 
 }
index fb31719..a0d80af 100644 (file)
@@ -26,15 +26,15 @@ import java.util.HashMap;
 import java.util.Map;
 import java.util.Map.Entry;
 
-import net.pterodactylus.sone.web.page.Page.Request;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Filter;
 import net.pterodactylus.util.template.TemplateContext;
 
 /**
- * This filter expects a {@link Request} as input and outputs a {@link URI} that
- * is modified by the parameters. The name of the parameter is handed in as
- * “name”, the value may either be stored in “value”, or in a template variable
- * whose key is stored in “key”.
+ * This filter expects a {@link FreenetRequest} as input and outputs a
+ * {@link URI} that is modified by the parameters. The name of the parameter is
+ * handed in as “name”, the value may either be stored in “value”, or in a
+ * template variable whose key is stored in “key”.
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
@@ -45,7 +45,7 @@ public class RequestChangeFilter implements Filter {
         */
        @Override
        public Object format(TemplateContext templateContext, Object data, Map<String, String> parameters) {
-               Request request = (Request) data;
+               FreenetRequest request = (FreenetRequest) data;
                String name = parameters.get("name");
                String nameKey = parameters.get("nameKey");
                if (nameKey != null) {
index 23b2227..ead6066 100644 (file)
@@ -25,6 +25,8 @@ import net.pterodactylus.sone.core.Core.SoneStatus;
 import net.pterodactylus.sone.data.Profile;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.freenet.wot.Trust;
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.ajax.GetTimesAjaxPage;
 import net.pterodactylus.util.logging.Logging;
 import net.pterodactylus.util.template.Accessor;
 import net.pterodactylus.util.template.ReflectionAccessor;
@@ -97,6 +99,8 @@ public class SoneAccessor extends ReflectionAccessor {
                        return core.isNewSone(sone.getId());
                } else if (member.equals("locked")) {
                        return core.isLocked(sone);
+               } else if (member.equals("lastUpdatedText")) {
+                       return GetTimesAjaxPage.getTime((WebInterface) templateContext.get("webInterface"), sone.getTime());
                } else if (member.equals("trust")) {
                        Sone currentSone = (Sone) templateContext.get("currentSone");
                        if (currentSone == null) {
diff --git a/src/main/java/net/pterodactylus/sone/template/UniqueElementFilter.java b/src/main/java/net/pterodactylus/sone/template/UniqueElementFilter.java
new file mode 100644 (file)
index 0000000..70dae70
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * Sone - UniqueElementFilter.java - Copyright © 2011 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.template;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import net.pterodactylus.util.template.Filter;
+import net.pterodactylus.util.template.TemplateContext;
+
+/**
+ * Filter that reduces a collection to a {@link Set}, removing duplicates.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class UniqueElementFilter implements Filter {
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public Object format(TemplateContext templateContext, Object data, Map<String, String> parameters) {
+               if (!(data instanceof Collection<?>)) {
+                       return data;
+               }
+               return new HashSet<Object>((Collection<?>) data);
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/text/FreenetLinkParser.java b/src/main/java/net/pterodactylus/sone/text/FreenetLinkParser.java
deleted file mode 100644 (file)
index 7c30b76..0000000
+++ /dev/null
@@ -1,372 +0,0 @@
-/*
- * Sone - FreenetLinkParser.java - Copyright © 2010 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.text;
-
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.Reader;
-import java.io.StringReader;
-import java.net.MalformedURLException;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-import net.pterodactylus.sone.core.Core;
-import net.pterodactylus.sone.data.Post;
-import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.template.SoneAccessor;
-import net.pterodactylus.util.logging.Logging;
-import net.pterodactylus.util.template.TemplateContextFactory;
-import net.pterodactylus.util.template.TemplateParser;
-import freenet.keys.FreenetURI;
-
-/**
- * {@link Parser} implementation that can recognize Freenet URIs.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class FreenetLinkParser implements Parser<FreenetLinkParserContext> {
-
-       /** The logger. */
-       private static final Logger logger = Logging.getLogger(FreenetLinkParser.class);
-
-       /** Pattern to detect whitespace. */
-       private static final Pattern whitespacePattern = Pattern.compile("[\\u000a\u0020\u00a0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u200c\u200d\u202f\u205f\u2060\u2800\u3000]");
-
-       /**
-        * Enumeration for all recognized link types.
-        *
-        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
-        */
-       private enum LinkType {
-
-               /** Link is a KSK. */
-               KSK,
-
-               /** Link is a CHK. */
-               CHK,
-
-               /** Link is an SSK. */
-               SSK,
-
-               /** Link is a USK. */
-               USK,
-
-               /** Link is HTTP. */
-               HTTP,
-
-               /** Link is HTTPS. */
-               HTTPS,
-
-               /** Link is a Sone. */
-               SONE,
-
-               /** Link is a post. */
-               POST,
-
-       }
-
-       /** The core. */
-       private final Core core;
-
-       /** The template factory. */
-       private final TemplateContextFactory templateContextFactory;
-
-       /**
-        * Creates a new freenet link parser.
-        *
-        * @param core
-        *            The core
-        * @param templateContextFactory
-        *            The template context factory
-        */
-       public FreenetLinkParser(Core core, TemplateContextFactory templateContextFactory) {
-               this.core = core;
-               this.templateContextFactory = templateContextFactory;
-       }
-
-       //
-       // PART METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public Part parse(FreenetLinkParserContext 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) {
-                                       continue;
-                               }
-                               parts.add(createPlainTextPart("\n"));
-                               ++emptyLines;
-                               lastLineEmpty = emptyLines == 2;
-                               continue;
-                       }
-                       emptyLines = 0;
-                       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(createPlainTextPart("\n" + line));
-                                       } else {
-                                               parts.add(createPlainTextPart(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;
-                               }
-                               if ((next >= 8) && (line.substring(next - 8, next).equals("freenet:"))) {
-                                       next -= 8;
-                                       line = line.substring(0, next) + line.substring(next + 8);
-                               }
-                               Matcher matcher = whitespacePattern.matcher(line);
-                               int nextSpace = matcher.find(next) ? matcher.start() : line.length();
-                               if (nextSpace > (next + 4)) {
-                                       if (!lastLineEmpty && lineComplete) {
-                                               parts.add(createPlainTextPart("\n" + line.substring(0, next)));
-                                       } else {
-                                               parts.add(createPlainTextPart(line.substring(0, next)));
-                                       }
-                                       String link = line.substring(next, nextSpace);
-                                       String name = link;
-                                       logger.log(Level.FINER, "Found link: %s", link);
-                                       logger.log(Level.FINEST, "Next: %d, CHK: %d, SSK: %d, USK: %d", new Object[] { next, 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 (name == null) {
-                                                               name = link.substring(0, Math.min(9, link.length()));
-                                                       }
-                                                       boolean fromPostingSone = (context.getPostingSone() != null) && ((linkType == LinkType.SSK) || (linkType == LinkType.USK)) && link.substring(4, Math.min(link.length(), 47)).equals(context.getPostingSone().getId());
-                                                       parts.add(fromPostingSone ? createTrustedFreenetLinkPart(link, name) : createFreenetLinkPart(link, name));
-                                               } catch (MalformedURLException mue1) {
-                                                       /* not a valid link, insert as plain text. */
-                                                       parts.add(createPlainTextPart(link));
-                                               } catch (NullPointerException npe1) {
-                                                       /* FreenetURI sometimes throws these, too. */
-                                                       parts.add(createPlainTextPart(link));
-                                               } catch (ArrayIndexOutOfBoundsException aioobe1) {
-                                                       /* oh, and these, too. */
-                                                       parts.add(createPlainTextPart(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('?'));
-                                               }
-                                               link = "?_CHECKED_HTTP_=" + link;
-                                               parts.add(createInternetLinkPart(link, name));
-                                       } else if (linkType == LinkType.SONE) {
-                                               String soneId = link.substring(7);
-                                               Sone sone = core.getSone(soneId, false);
-                                               if (sone != null) {
-                                                       parts.add(createInSoneLinkPart("viewSone.html?sone=" + soneId, SoneAccessor.getNiceName(sone)));
-                                               } else {
-                                                       parts.add(createPlainTextPart(link));
-                                               }
-                                       } else if (linkType == LinkType.POST) {
-                                               String postId = link.substring(7);
-                                               Post post = core.getPost(postId, false);
-                                               if (post != null) {
-                                                       String postText = post.getText();
-                                                       postText = postText.substring(0, Math.min(postText.length(), 20)) + "…";
-                                                       Sone postSone = post.getSone();
-                                                       parts.add(createInSoneLinkPart("viewPost.html?post=" + postId, postText, (postSone == null) ? postText : SoneAccessor.getNiceName(post.getSone())));
-                                               } else {
-                                                       parts.add(createPlainTextPart(link));
-                                               }
-                                       }
-                                       line = line.substring(nextSpace);
-                               } else {
-                                       if (!lastLineEmpty && lineComplete) {
-                                               parts.add(createPlainTextPart("\n" + line.substring(0, next + 4)));
-                                       } else {
-                                               parts.add(createPlainTextPart(line.substring(0, next + 4)));
-                                       }
-                                       line = line.substring(next + 4);
-                               }
-                               lineComplete = false;
-                       }
-                       lastLineEmpty = false;
-               }
-               for (int partIndex = parts.size() - 1; partIndex >= 0; --partIndex) {
-                       if (!parts.getPart(partIndex).toString().equals("\n")) {
-                               break;
-                       }
-                       parts.removePart(partIndex);
-               }
-               return parts;
-       }
-
-       //
-       // PRIVATE METHODS
-       //
-
-       /**
-        * Creates a new plain text part based on a template.
-        *
-        * @param text
-        *            The text to display
-        * @return The part that displays the given text
-        */
-       private Part createPlainTextPart(String text) {
-               return new TemplatePart(templateContextFactory, TemplateParser.parse(new StringReader("<% text|html>"))).set("text", text);
-       }
-
-       /**
-        * Creates a new part based on a template that links to a site within the
-        * normal internet.
-        *
-        * @param link
-        *            The target of the link
-        * @param name
-        *            The name of the link
-        * @return The part that displays the link
-        */
-       private Part createInternetLinkPart(String link, String name) {
-               return new TemplatePart(templateContextFactory, TemplateParser.parse(new StringReader("<a class=\"internet\" href=\"/<% link|html>\" title=\"<% link|html>\"><% name|html></a>"))).set("link", link).set("name", name);
-       }
-
-       /**
-        * Creates a new part based on a template that links to a site within
-        * freenet.
-        *
-        * @param link
-        *            The target of the link
-        * @param name
-        *            The name of the link
-        * @return The part that displays the link
-        */
-       private Part createFreenetLinkPart(String link, String name) {
-               return new TemplatePart(templateContextFactory, TemplateParser.parse(new StringReader("<a class=\"freenet\" href=\"/<% link|html>\" title=\"<% link|html>\"><% name|html></a>"))).set("link", link).set("name", name);
-       }
-
-       /**
-        * Creates a new part based on a template that links to a page in the
-        * poster’s keyspace.
-        *
-        * @param link
-        *            The target of the link
-        * @param name
-        *            The name of the link
-        * @return The part that displays the link
-        */
-       private Part createTrustedFreenetLinkPart(String link, String name) {
-               return new TemplatePart(templateContextFactory, TemplateParser.parse(new StringReader("<a class=\"freenet-trusted\" href=\"/<% link|html>\" title=\"<% link|html>\"><% name|html></a>"))).set("link", link).set("name", name);
-       }
-
-       /**
-        * Creates a new part based on a template that links to a page in Sone.
-        *
-        * @param link
-        *            The target of the link
-        * @param name
-        *            The name of the link
-        * @return The part that displays the link
-        */
-       private Part createInSoneLinkPart(String link, String name) {
-               return createInSoneLinkPart(link, name, name);
-       }
-
-       /**
-        * Creates a new part based on a template that links to a page in Sone.
-        *
-        * @param link
-        *            The target of the link
-        * @param name
-        *            The name of the link
-        * @param title
-        *            The title attribute of the link
-        * @return The part that displays the link
-        */
-       private Part createInSoneLinkPart(String link, String name, String title) {
-               return new TemplatePart(templateContextFactory, TemplateParser.parse(new StringReader("<a class=\"in-sone\" href=\"<%link|html>\" title=\"<%title|html>\"><%name|html></a>"))).set("link", link).set("name", name).set("title", title);
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/text/FreenetLinkParserContext.java b/src/main/java/net/pterodactylus/sone/text/FreenetLinkParserContext.java
deleted file mode 100644 (file)
index e524d91..0000000
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Sone - FreenetLinkParserContext.java - Copyright © 2011 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.text;
-
-import net.pterodactylus.sone.data.Sone;
-
-/**
- * {@link ParserContext} implementation for the {@link FreenetLinkParser}. It
- * stores the {@link Sone} that provided the parsed text so that certain links
- * can be marked in a different way.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class FreenetLinkParserContext implements ParserContext {
-
-       /** The posting Sone. */
-       private final Sone postingSone;
-
-       /**
-        * Creates a new link parser context.
-        *
-        * @param postingSone
-        *            The posting Sone
-        */
-       public FreenetLinkParserContext(Sone postingSone) {
-               this.postingSone = postingSone;
-       }
-
-       /**
-        * Returns the Sone that provided the text that is being parsed.
-        *
-        * @return The posting Sone
-        */
-       public Sone getPostingSone() {
-               return postingSone;
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/text/FreenetLinkPart.java b/src/main/java/net/pterodactylus/sone/text/FreenetLinkPart.java
new file mode 100644 (file)
index 0000000..be3aef9
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+ * Sone - FreenetLinkPart.java - Copyright © 2011 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.text;
+
+/**
+ * {@link LinkPart} implementation that stores an additional attribute: if the
+ * link is an SSK or USK link and the post was created by an identity that owns
+ * the keyspace in question.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class FreenetLinkPart extends LinkPart {
+
+       /** Whether the link is trusted. */
+       private final boolean trusted;
+
+       /**
+        * Creates a new freenet link part.
+        *
+        * @param link
+        *            The link of the part
+        * @param text
+        *            The text of the part
+        * @param trusted
+        *            {@code true} if the link is trusted, {@code false} otherwise
+        */
+       public FreenetLinkPart(String link, String text, boolean trusted) {
+               this(link, text, text, trusted);
+       }
+
+       /**
+        * Creates a new freenet link part.
+        *
+        * @param link
+        *            The link of the part
+        * @param text
+        *            The text of the part
+        * @param title
+        *            The title of the part
+        * @param trusted
+        *            {@code true} if the link is trusted, {@code false} otherwise
+        */
+       public FreenetLinkPart(String link, String text, String title, boolean trusted) {
+               super(link, text, title);
+               this.trusted = trusted;
+       }
+
+       //
+       // ACCESSORS
+       //
+
+       /**
+        * Returns whether the link is trusted.
+        *
+        * @return {@code true} if the link is trusted, {@code false} otherwise
+        */
+       public boolean isTrusted() {
+               return trusted;
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/text/LinkPart.java b/src/main/java/net/pterodactylus/sone/text/LinkPart.java
new file mode 100644 (file)
index 0000000..d5da730
--- /dev/null
@@ -0,0 +1,97 @@
+/*
+ * Sone - LinkPart.java - Copyright © 2011 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.text;
+
+/**
+ * {@link Part} implementation that can hold a link. A link contains of three
+ * attributes: the link itself, the text that is shown instead of the link, and
+ * an explanatory text that can be displayed e.g. as a tooltip.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class LinkPart implements Part {
+
+       /** The link of this part. */
+       private final String link;
+
+       /** The text of this part. */
+       private final String text;
+
+       /** The title of this part. */
+       private final String title;
+
+       /**
+        * Creates a new link part.
+        *
+        * @param link
+        *            The link of the link part
+        * @param text
+        *            The text of the link part
+        */
+       public LinkPart(String link, String text) {
+               this(link, text, text);
+       }
+
+       /**
+        * Creates a new link part.
+        *
+        * @param link
+        *            The link of the link part
+        * @param text
+        *            The text of the link part
+        * @param title
+        *            The title of the link part
+        */
+       public LinkPart(String link, String text, String title) {
+               this.link = link;
+               this.text = text;
+               this.title = title;
+       }
+
+       //
+       // ACCESSORS
+       //
+
+       /**
+        * Returns the link of this part.
+        *
+        * @return The link of this part
+        */
+       public String getLink() {
+               return link;
+       }
+
+       /**
+        * Returns the text of this part.
+        *
+        * @return The text of this part
+        */
+       public String getText() {
+               return text;
+       }
+
+       /**
+        * Returns the title of this part.
+        *
+        * @return The title of this part
+        */
+       public String getTitle() {
+               return title;
+       }
+
+}
index e43ed47..87cdab5 100644 (file)
@@ -31,16 +31,16 @@ import java.io.Reader;
 public interface Parser<C extends ParserContext> {
 
        /**
-        * Create a {@link Part} from the given text source.
+        * Create one or more {@link Part}s from the given text source.
         *
         * @param context
-        *            The parser context
+        *            The parser context (may be {@code null})
         * @param source
         *            The text source
-        * @return The parsed part
+        * @return The parsed parts
         * @throws IOException
         *             if an I/O error occurs
         */
-       public Part parse(C context, Reader source) throws IOException;
+       public Iterable<Part> parse(C context, Reader source) throws IOException;
 
 }
index 7f38ad8..ee61b6f 100644 (file)
@@ -1,3 +1,19 @@
+/*
+ * Sone - ParserContext.java - Copyright © 2010 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.text;
 
index b9083b7..e16dfb4 100644 (file)
 
 package net.pterodactylus.sone.text;
 
-import net.pterodactylus.util.io.Renderable;
-
 /**
  * A part is a single piece of information that can be displayed as a single
- * element.
+ * element. How the part is displayed is not part of the {@code Part}
+ * specification.
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
-public interface Part extends Renderable {
+public interface Part {
 
-       /* all required methods are inherited from {@link Renderable}. */
+       /* no methods. */
 
 }
index d52658e..88c889e 100644 (file)
 
 package net.pterodactylus.sone.text;
 
-import java.io.IOException;
-import java.io.StringWriter;
-import java.io.Writer;
+import java.util.ArrayDeque;
 import java.util.ArrayList;
+import java.util.Deque;
+import java.util.Iterator;
 import java.util.List;
+import java.util.NoSuchElementException;
 
 /**
  * Part implementation that can contain an arbitrary amount of other parts.
@@ -30,7 +31,7 @@ import java.util.List;
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
-public class PartContainer implements Part {
+public class PartContainer implements Part, Iterable<Part> {
 
        /** The parts to render. */
        private final List<Part> parts = new ArrayList<Part>();
@@ -80,35 +81,70 @@ public class PartContainer implements Part {
        }
 
        //
-       // PART METHODS
+       // ITERABLE METHODS
        //
 
        /**
         * {@inheritDoc}
         */
        @Override
-       public void render(Writer writer) throws IOException {
-               for (Part part : parts) {
-                       part.render(writer);
-               }
-       }
+       @SuppressWarnings("synthetic-access")
+       public Iterator<Part> iterator() {
+               return new Iterator<Part>() {
 
-       //
-       // OBJECT METHODS
-       //
+                       private Deque<Iterator<Part>> partStack = new ArrayDeque<Iterator<Part>>();
+                       private Part nextPart;
+                       private boolean foundNextPart;
+                       private boolean noNextPart;
 
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public String toString() {
-               StringWriter stringWriter = new StringWriter();
-               try {
-                       render(stringWriter);
-               } catch (IOException ioe1) {
-                       /* should never throw, ignore. */
-               }
-               return stringWriter.toString();
+                       {
+                               partStack.push(parts.iterator());
+                       }
+
+                       private void findNext() {
+                               if (foundNextPart) {
+                                       return;
+                               }
+                               noNextPart = true;
+                               while (!partStack.isEmpty()) {
+                                       @SuppressWarnings("hiding")
+                                       Iterator<Part> parts = partStack.pop();
+                                       if (parts.hasNext()) {
+                                               nextPart = parts.next();
+                                               partStack.push(parts);
+                                               if (nextPart instanceof PartContainer) {
+                                                       partStack.push(((PartContainer) nextPart).iterator());
+                                               } else {
+                                                       noNextPart = false;
+                                                       break;
+                                               }
+                                       }
+                               }
+                               foundNextPart = true;
+                       }
+
+                       @Override
+                       public boolean hasNext() {
+                               findNext();
+                               return !noNextPart;
+                       }
+
+                       @Override
+                       public Part next() {
+                               findNext();
+                               if (noNextPart) {
+                                       throw new NoSuchElementException();
+                               }
+                               foundNextPart = false;
+                               return nextPart;
+                       }
+
+                       @Override
+                       public void remove() {
+                               /* ignore. */
+                       }
+
+               };
        }
 
 }
diff --git a/src/main/java/net/pterodactylus/sone/text/PlainTextPart.java b/src/main/java/net/pterodactylus/sone/text/PlainTextPart.java
new file mode 100644 (file)
index 0000000..a1f2088
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+ * Sone - PlainTextPart.java - Copyright © 2011 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.text;
+
+/**
+ * {@link Part} implementation that holds a single piece of text.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class PlainTextPart implements Part {
+
+       /** The text of the part. */
+       private final String text;
+
+       /**
+        * Creates a new plain-text part.
+        *
+        * @param text
+        *            The text of the part
+        */
+       public PlainTextPart(String text) {
+               this.text = text;
+       }
+
+       //
+       // ACCESSORS
+       //
+
+       /**
+        * Returns the text of this part.
+        *
+        * @return The text of this part
+        */
+       public String getText() {
+               return text;
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/text/PostPart.java b/src/main/java/net/pterodactylus/sone/text/PostPart.java
new file mode 100644 (file)
index 0000000..22acd43
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * Sone - PostLinkPart.java - Copyright © 2011 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.text;
+
+import net.pterodactylus.sone.data.Post;
+
+/**
+ * {@link Part} implementation that stores a reference to a {@link Post}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class PostPart implements Part {
+
+       /** The post this part refers to. */
+       private final Post post;
+
+       /**
+        * Creates a new post part.
+        *
+        * @param post
+        *            The referenced post
+        */
+       public PostPart(Post post) {
+               this.post = post;
+       }
+
+       //
+       // ACCESSORS
+       //
+
+       /**
+        * Returns the post referenced by this part.
+        *
+        * @return The post referenced by this part
+        */
+       public Post getPost() {
+               return post;
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/text/SonePart.java b/src/main/java/net/pterodactylus/sone/text/SonePart.java
new file mode 100644 (file)
index 0000000..5c6a63d
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * Sone - SoneLinkPart.java - Copyright © 2011 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.text;
+
+import net.pterodactylus.sone.data.Sone;
+
+/**
+ * {@link Part} implementation that stores a reference to a {@link Sone}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class SonePart implements Part {
+
+       /** The referenced {@link Sone}. */
+       private final Sone sone;
+
+       /**
+        * Creates a new Sone part.
+        *
+        * @param sone
+        *            The referenced Sone
+        */
+       public SonePart(Sone sone) {
+               this.sone = sone;
+       }
+
+       //
+       // ACCESSORS
+       //
+
+       /**
+        * Returns the referenced Sone.
+        *
+        * @return The referenced Sone
+        */
+       public Sone getSone() {
+               return sone;
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/text/SoneTextParser.java b/src/main/java/net/pterodactylus/sone/text/SoneTextParser.java
new file mode 100644 (file)
index 0000000..051c02a
--- /dev/null
@@ -0,0 +1,301 @@
+/*
+ * Sone - FreenetLinkParser.java - Copyright © 2010 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.text;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.Reader;
+import java.net.MalformedURLException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import net.pterodactylus.sone.core.PostProvider;
+import net.pterodactylus.sone.core.SoneProvider;
+import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.util.logging.Logging;
+import freenet.keys.FreenetURI;
+
+/**
+ * {@link Parser} implementation that can recognize Freenet URIs.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class SoneTextParser implements Parser<SoneTextParserContext> {
+
+       /** The logger. */
+       private static final Logger logger = Logging.getLogger(SoneTextParser.class);
+
+       /** Pattern to detect whitespace. */
+       private static final Pattern whitespacePattern = Pattern.compile("[\\u000a\u0020\u00a0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u200c\u200d\u202f\u205f\u2060\u2800\u3000]");
+
+       /**
+        * Enumeration for all recognized link types.
+        *
+        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+        */
+       private enum LinkType {
+
+               /** Link is a KSK. */
+               KSK,
+
+               /** Link is a CHK. */
+               CHK,
+
+               /** Link is an SSK. */
+               SSK,
+
+               /** Link is a USK. */
+               USK,
+
+               /** Link is HTTP. */
+               HTTP,
+
+               /** Link is HTTPS. */
+               HTTPS,
+
+               /** Link is a Sone. */
+               SONE,
+
+               /** Link is a post. */
+               POST,
+
+       }
+
+       /** The Sone provider. */
+       private final SoneProvider soneProvider;
+
+       /** The post provider. */
+       private final PostProvider postProvider;
+
+       /**
+        * Creates a new freenet link parser.
+        *
+        * @param soneProvider
+        *            The Sone provider
+        * @param postProvider
+        *            The post provider
+        */
+       public SoneTextParser(SoneProvider soneProvider, PostProvider postProvider) {
+               this.soneProvider = soneProvider;
+               this.postProvider = postProvider;
+       }
+
+       //
+       // PART METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       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) {
+                                       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));
+                                       }
+                                       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);
+                               }
+
+                               /* 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) && (sone.getName() != null)) {
+                                                       parts.add(new SonePart(sone));
+                                               } else {
+                                                       parts.add(new PlainTextPart(line.substring(0, 50)));
+                                               }
+                                               line = line.substring(50);
+                                       } else {
+                                               parts.add(new PlainTextPart(line));
+                                               line = "";
+                                       }
+                                       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));
+                                               } else {
+                                                       parts.add(new PlainTextPart(line.substring(0, 43)));
+                                               }
+                                               line = line.substring(43);
+                                       } else {
+                                               parts.add(new PlainTextPart(line));
+                                               line = "";
+                                       }
+                                       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, "Found link: %s", link);
+                               logger.log(Level.FINEST, "CHK: %d, SSK: %d, USK: %d", new Object[] { 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 (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));
+                               }
+                               line = line.substring(nextSpace);
+                       }
+                       lastLineEmpty = false;
+               }
+               for (int partIndex = parts.size() - 1; partIndex >= 0; --partIndex) {
+                       Part part = parts.getPart(partIndex);
+                       if (!(part instanceof PlainTextPart) || !"\n".equals(((PlainTextPart) part).getText())) {
+                               break;
+                       }
+                       parts.removePart(partIndex);
+               }
+               return parts;
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/text/SoneTextParserContext.java b/src/main/java/net/pterodactylus/sone/text/SoneTextParserContext.java
new file mode 100644 (file)
index 0000000..35b190b
--- /dev/null
@@ -0,0 +1,69 @@
+/*
+ * Sone - SoneTextParserContext.java - Copyright © 2011 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.text;
+
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.page.FreenetRequest;
+
+/**
+ * {@link ParserContext} implementation for the {@link SoneTextParser}. It
+ * stores the {@link Sone} that provided the parsed text so that certain links
+ * can be marked in a different way.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class SoneTextParserContext implements ParserContext {
+
+       /** The request being processed. */
+       private final FreenetRequest request;
+
+       /** The posting Sone. */
+       private final Sone postingSone;
+
+       /**
+        * Creates a new link parser context.
+        *
+        * @param request
+        *            The request being processed
+        * @param postingSone
+        *            The posting Sone
+        */
+       public SoneTextParserContext(FreenetRequest request, Sone postingSone) {
+               this.request = request;
+               this.postingSone = postingSone;
+       }
+
+       /**
+        * Returns the request that is currently being processed.
+        *
+        * @return The request being processed
+        */
+       public FreenetRequest getRequest() {
+               return request;
+       }
+
+       /**
+        * Returns the Sone that provided the text that is being parsed.
+        *
+        * @return The posting Sone
+        */
+       public Sone getPostingSone() {
+               return postingSone;
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/text/TemplatePart.java b/src/main/java/net/pterodactylus/sone/text/TemplatePart.java
deleted file mode 100644 (file)
index ac5694c..0000000
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- * Sone - TemplatePart.java - Copyright © 2010 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.text;
-
-import java.io.IOException;
-import java.io.StringWriter;
-import java.io.Writer;
-
-import net.pterodactylus.util.template.Template;
-import net.pterodactylus.util.template.TemplateContext;
-import net.pterodactylus.util.template.TemplateContextFactory;
-import net.pterodactylus.util.template.TemplateException;
-
-/**
- * {@link Part} implementation that is rendered using a {@link Template}.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class TemplatePart implements Part, net.pterodactylus.util.template.Part {
-
-       /** The template context factory. */
-       private final TemplateContextFactory templateContextFactory;
-
-       /** The template to render for this part. */
-       private final Template template;
-
-       /**
-        * Creates a new template part.
-        *
-        * @param templateContextFactory
-        *            The template context factory
-        * @param template
-        *            The template to render
-        */
-       public TemplatePart(TemplateContextFactory templateContextFactory, Template template) {
-               this.templateContextFactory = templateContextFactory;
-               this.template = template;
-       }
-
-       //
-       // ACTIONS
-       //
-
-       /**
-        * Sets a variable in the template.
-        *
-        * @param key
-        *            The key of the variable
-        * @param value
-        *            The value of the variable
-        * @return This template part (for method chaining)
-        */
-       public TemplatePart set(String key, Object value) {
-               template.getInitialContext().set(key, value);
-               return this;
-       }
-
-       //
-       // PART METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public void render(Writer writer) throws IOException {
-               template.render(templateContextFactory.createTemplateContext().mergeContext(template.getInitialContext()), writer);
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public void render(TemplateContext templateContext, Writer writer) throws TemplateException {
-               template.render(templateContext.mergeContext(template.getInitialContext()), writer);
-       }
-
-       //
-       // OBJECT METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public String toString() {
-               StringWriter stringWriter = new StringWriter();
-               try {
-                       render(stringWriter);
-               } catch (IOException ioe1) {
-                       /* should never throw, ignore. */
-               }
-               return stringWriter.toString();
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/text/TextFilter.java b/src/main/java/net/pterodactylus/sone/text/TextFilter.java
new file mode 100644 (file)
index 0000000..9e3587c
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ * Sone - TextFilter.java - Copyright © 2011 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.text;
+
+/**
+ * Filter for newly inserted text. This filter strips HTTP links to the local
+ * node of identifying marks, e.g. a link to “http://localhost:8888/KSK@gpl.txt”
+ * will be converted to “KSK@gpl.txt”. This will only work for links that point
+ * to the same address Sone is accessed by, so if you access Sone using
+ * localhost:8888, links to 127.0.0.1:8888 will not be removed.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class TextFilter {
+
+       /**
+        * Filters the given text, stripping the host header part for links to the
+        * local node.
+        *
+        * @param hostHeader
+        *            The host header from the request
+        * @param text
+        *            The text to filter
+        * @return The filtered text
+        */
+       public static String filter(String hostHeader, String text) {
+
+               /* filter http(s) links to own node. */
+               if (hostHeader != null) {
+                       String line = text;
+                       for (String toRemove : new String[] { "http://" + hostHeader + "/", "https://" + hostHeader + "/", "http://" + hostHeader, "https://" + hostHeader }) {
+                               while (line.indexOf(toRemove) != -1) {
+                                       line = line.replace(toRemove, "");
+                               }
+                       }
+                       return line;
+               }
+
+               /* not modified. */
+               return text;
+       }
+
+}
index 4c14457..a9698e4 100644 (file)
@@ -17,6 +17,7 @@
 
 package net.pterodactylus.sone.web;
 
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
 import net.pterodactylus.util.version.Version;
@@ -54,7 +55,7 @@ public class AboutPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                templateContext.set("version", version);
        }
index 12a0934..0ebf9d5 100644 (file)
 
 package net.pterodactylus.sone.web;
 
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
 
 /**
  * Page that lets the user bookmark a post.
@@ -46,7 +47,7 @@ public class BookmarkPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                if (request.getMethod() == Method.POST) {
                        String id = request.getHttpRequest().getPartAsStringFailsafe("post", 36);
index d6f63aa..ad1717b 100644 (file)
@@ -23,6 +23,7 @@ import java.util.List;
 import java.util.Set;
 
 import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.collection.Pagination;
 import net.pterodactylus.util.filter.Filter;
 import net.pterodactylus.util.filter.Filters;
@@ -57,7 +58,7 @@ public class BookmarksPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                Set<Post> allPosts = webInterface.getCore().getBookmarkedPosts();
                Set<Post> loadedPosts = Filters.filteredSet(allPosts, new Filter<Post>() {
index afe95b7..a695952 100644 (file)
@@ -19,9 +19,10 @@ package net.pterodactylus.sone.web;
 
 import net.pterodactylus.sone.data.Album;
 import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
 
 /**
  * Page that lets the user create a new album.
@@ -50,7 +51,7 @@ public class CreateAlbumPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                if (request.getMethod() == Method.POST) {
                        String name = request.getHttpRequest().getPartAsStringFailsafe("name", 64).trim();
@@ -64,7 +65,7 @@ public class CreateAlbumPage extends SoneTemplatePage {
                        Album parent = webInterface.getCore().getAlbum(parentId, false);
                        Album album = webInterface.getCore().createAlbum(currentSone, parent);
                        album.setTitle(name).setDescription(description);
-                       webInterface.getCore().saveSone(currentSone);
+                       webInterface.getCore().touchConfiguration();
                        throw new RedirectException("imageBrowser.html?album=" + album.getId());
                }
        }
index 57eb18a..22f6efa 100644 (file)
@@ -19,9 +19,11 @@ package net.pterodactylus.sone.web;
 
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.text.TextFilter;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
 
 /**
  * This page lets the user create a new {@link Post}.
@@ -50,7 +52,7 @@ public class CreatePostPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
                if (request.getMethod() == Method.POST) {
@@ -64,6 +66,7 @@ public class CreatePostPage extends SoneTemplatePage {
                                        sender = currentSone;
                                }
                                Sone recipient = webInterface.getCore().getSone(recipientId, false);
+                               text = TextFilter.filter(request.getHttpRequest().getHeader("host"), text);
                                webInterface.getCore().createPost(sender, recipient, System.currentTimeMillis(), text);
                                throw new RedirectException(returnPage);
                        }
index aae5e83..f28be35 100644 (file)
@@ -19,9 +19,11 @@ package net.pterodactylus.sone.web;
 
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.text.TextFilter;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
 
 /**
  * This page lets the user post a reply to a post.
@@ -50,7 +52,7 @@ public class CreateReplyPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                String postId = request.getHttpRequest().getPartAsStringFailsafe("post", 36);
                String text = request.getHttpRequest().getPartAsStringFailsafe("text", 65536).trim();
@@ -63,6 +65,7 @@ public class CreateReplyPage extends SoneTemplatePage {
                                if (sender == null) {
                                        sender = getCurrentSone(request.getToadletContext());
                                }
+                               text = TextFilter.filter(request.getHttpRequest().getHeader("host"), text);
                                webInterface.getCore().createReply(sender, post, text);
                                throw new RedirectException(returnPage);
                        }
index cc40320..9878358 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * FreenetSone - CreateSonePage.java - Copyright © 2010 David Roden
+ * Sone - CreateSonePage.java - Copyright © 2010 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
@@ -28,10 +28,11 @@ import java.util.logging.Logger;
 import net.pterodactylus.sone.core.Core;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.freenet.wot.OwnIdentity;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.logging.Logging;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
 import freenet.clients.http.ToadletContext;
 
 /**
@@ -94,7 +95,7 @@ public class CreateSonePage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                List<OwnIdentity> ownIdentitiesWithoutSone = getOwnIdentitiesWithoutSone(webInterface.getCore());
                templateContext.set("identitiesWithoutSone", ownIdentitiesWithoutSone);
@@ -129,6 +130,9 @@ public class CreateSonePage extends SoneTemplatePage {
         */
        @Override
        public boolean isEnabled(ToadletContext toadletContext) {
+               if (webInterface.getCore().getPreferences().isRequireFullAccess() && !toadletContext.isAllowedFullAccess()) {
+                       return false;
+               }
                return (getCurrentSone(toadletContext, false) == null) || (webInterface.getCore().getLocalSones().size() == 1);
        }
 
index 6855e38..d03e065 100644 (file)
 package net.pterodactylus.sone.web;
 
 import net.pterodactylus.sone.data.Album;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
 
 /**
  * Page that lets the user delete an {@link Album}.
@@ -45,7 +46,7 @@ public class DeleteAlbumPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                if (request.getMethod() == Method.POST) {
                        String albumId = request.getHttpRequest().getPartAsStringFailsafe("album", 36);
index 999a38a..66098ff 100644 (file)
 package net.pterodactylus.sone.web;
 
 import net.pterodactylus.sone.data.Image;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
 
 /**
  * Page that lets the user delete an {@link Image}.
@@ -49,7 +50,7 @@ public class DeleteImagePage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                String imageId = (request.getMethod() == Method.POST) ? request.getHttpRequest().getPartAsStringFailsafe("image", 36) : request.getHttpRequest().getParam("image");
                Image image = webInterface.getCore().getImage(imageId, false);
index 7a36d02..c8fd20f 100644 (file)
 package net.pterodactylus.sone.web;
 
 import net.pterodactylus.sone.data.Post;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
 
 /**
  * Lets the user delete a post they made.
@@ -49,7 +50,7 @@ public class DeletePostPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                if (request.getMethod() == Method.GET) {
                        String postId = request.getHttpRequest().getParam("post");
index 82f2ace..030279b 100644 (file)
@@ -20,9 +20,10 @@ package net.pterodactylus.sone.web;
 import net.pterodactylus.sone.data.Profile;
 import net.pterodactylus.sone.data.Profile.Field;
 import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
 
 /**
  * Page that lets the user confirm the deletion of a profile field.
@@ -51,7 +52,7 @@ public class DeleteProfileFieldPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                Sone currentSone = getCurrentSone(request.getToadletContext());
                Profile profile = currentSone.getProfile();
index 782589f..226e0d3 100644 (file)
 package net.pterodactylus.sone.web;
 
 import net.pterodactylus.sone.data.Reply;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
 
 /**
  * This page lets the user delete a reply.
@@ -49,7 +50,7 @@ public class DeleteReplyPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                String replyId = request.getHttpRequest().getPartAsStringFailsafe("reply", 36);
                Reply reply = webInterface.getCore().getReply(replyId);
index 54d77b8..bbf533f 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * FreenetSone - DeleteSonePage.java - Copyright © 2010 David Roden
+ * Sone - DeleteSonePage.java - Copyright © 2010 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
 package net.pterodactylus.sone.web;
 
 import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
 
 /**
  * Lets the user delete a Sone. Of course the Sone is not really deleted from
@@ -51,7 +52,7 @@ public class DeleteSonePage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                if (request.getMethod() == Method.POST) {
                        if (request.getHttpRequest().isPartSet("deleteSone")) {
index 15c5675..9d79195 100644 (file)
@@ -17,6 +17,7 @@
 
 package net.pterodactylus.sone.web;
 
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.notify.Notification;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
@@ -48,7 +49,7 @@ public class DismissNotificationPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                String notificationId = request.getHttpRequest().getPartAsStringFailsafe("notification", 36);
                Notification notification = webInterface.getNotifications().getNotification(notificationId);
index c055545..473bf1c 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - TrustPage.java - Copyright © 2011 David Roden
+ * Sone - DistrustPage.java - Copyright © 2011 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
@@ -19,9 +19,10 @@ package net.pterodactylus.sone.web;
 
 import net.pterodactylus.sone.core.Core;
 import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
 
 /**
  * Page that lets the user distrust another Sone. This will assign a
@@ -52,7 +53,7 @@ public class DistrustPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                if (request.getMethod() == Method.POST) {
                        String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
index eaf8bf6..c3c2525 100644 (file)
 package net.pterodactylus.sone.web;
 
 import net.pterodactylus.sone.data.Album;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
 
 /**
  * TODO
@@ -43,7 +44,7 @@ public class EditAlbumPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                if (request.getMethod() == Method.POST) {
                        String albumId = request.getHttpRequest().getPartAsStringFailsafe("album", 36);
@@ -61,7 +62,7 @@ public class EditAlbumPage extends SoneTemplatePage {
                        }
                        String description = request.getHttpRequest().getPartAsStringFailsafe("description", 1000).trim();
                        album.setTitle(title).setDescription(description);
-                       webInterface.getCore().saveSone(album.getSone());
+                       webInterface.getCore().touchConfiguration();
                        throw new RedirectException("imageBrowser.html?album=" + album.getId());
                }
        }
index eab3bb9..afb8787 100644 (file)
 package net.pterodactylus.sone.web;
 
 import net.pterodactylus.sone.data.Image;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
 
 /**
  * Page that lets the user edit title and description of an {@link Image}.
@@ -49,7 +50,7 @@ public class EditImagePage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                if (request.getMethod() == Method.POST) {
                        String imageId = request.getHttpRequest().getPartAsStringFailsafe("image", 36);
@@ -67,7 +68,7 @@ public class EditImagePage extends SoneTemplatePage {
                        }
                        image.setTitle(title);
                        image.setDescription(description);
-                       webInterface.getCore().saveSone(image.getSone());
+                       webInterface.getCore().touchConfiguration();
                        throw new RedirectException("imageBrowser.html?image=" + image.getId());
                }
        }
index 219bdc5..a06592a 100644 (file)
@@ -20,9 +20,10 @@ package net.pterodactylus.sone.web;
 import net.pterodactylus.sone.data.Profile;
 import net.pterodactylus.sone.data.Profile.Field;
 import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
 
 /**
  * Page that lets the user edit the name of a profile field.
@@ -51,7 +52,7 @@ public class EditProfileFieldPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                Sone currentSone = getCurrentSone(request.getToadletContext());
                Profile profile = currentSone.getProfile();
index 52468a3..59c7e3e 100644 (file)
@@ -22,10 +22,11 @@ import java.util.List;
 import net.pterodactylus.sone.data.Profile;
 import net.pterodactylus.sone.data.Profile.Field;
 import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.number.Numbers;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
 import freenet.clients.http.ToadletContext;
 
 /**
@@ -55,7 +56,7 @@ public class EditProfilePage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                ToadletContext toadletContenxt = request.getToadletContext();
                Sone currentSone = getCurrentSone(toadletContenxt);
@@ -84,7 +85,7 @@ public class EditProfilePage extends SoneTemplatePage {
                                        field.setValue(value);
                                }
                                currentSone.setProfile(profile);
-                               webInterface.getCore().saveSone(currentSone);
+                               webInterface.getCore().touchConfiguration();
                                throw new RedirectException("editProfile.html");
                        } else if (request.getHttpRequest().getPartAsStringFailsafe("add-field", 4).equals("true")) {
                                String fieldName = request.getHttpRequest().getPartAsStringFailsafe("field-name", 256).trim();
@@ -92,7 +93,7 @@ public class EditProfilePage extends SoneTemplatePage {
                                        profile.addField(fieldName);
                                        currentSone.setProfile(profile);
                                        fields = profile.getFields();
-                                       webInterface.getCore().saveSone(currentSone);
+                                       webInterface.getCore().touchConfiguration();
                                        throw new RedirectException("editProfile.html#profile-fields");
                                } catch (IllegalArgumentException iae1) {
                                        templateContext.set("fieldName", fieldName);
@@ -153,7 +154,7 @@ public class EditProfilePage extends SoneTemplatePage {
         * @return The parsed ID, or {@code null} if there was no part matching the
         *         given string
         */
-       private String getFieldId(Request request, String partNameStart) {
+       private String getFieldId(FreenetRequest request, String partNameStart) {
                for (String partName : request.getHttpRequest().getParts()) {
                        if (partName.startsWith(partNameStart)) {
                                return partName.substring(partNameStart.length());
index 9cd1f2a..143e0ba 100644 (file)
 package net.pterodactylus.sone.web;
 
 import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
 
 /**
  * This page lets the user follow another Sone.
@@ -47,14 +48,16 @@ public class FollowSonePage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                if (request.getMethod() == Method.POST) {
-                       String soneId = request.getHttpRequest().getPartAsStringFailsafe("sone", 44);
                        String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
                        Sone currentSone = getCurrentSone(request.getToadletContext());
-                       currentSone.addFriend(soneId);
-                       webInterface.getCore().saveSone(currentSone);
+                       String soneIds = request.getHttpRequest().getPartAsStringFailsafe("sone", 1200);
+                       for (String soneId : soneIds.split("[ ,]+")) {
+                               currentSone.addFriend(soneId);
+                       }
+                       webInterface.getCore().touchConfiguration();
                        throw new RedirectException(returnPage);
                }
        }
index fdd72a8..29556c9 100644 (file)
 
 package net.pterodactylus.sone.web;
 
+import java.io.IOException;
+
 import net.pterodactylus.sone.data.TemporaryImage;
-import net.pterodactylus.sone.web.page.Page;
+import net.pterodactylus.sone.web.page.FreenetRequest;
+import net.pterodactylus.util.web.Page;
+import net.pterodactylus.util.web.Response;
 
 /**
  * Page that delivers a {@link TemporaryImage} to the browser.
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
-public class GetImagePage implements Page {
+public class GetImagePage implements Page<FreenetRequest> {
 
        /** The Sone web interface. */
        private final WebInterface webInterface;
@@ -52,13 +56,22 @@ public class GetImagePage implements Page {
         * {@inheritDoc}
         */
        @Override
-       public Response handleRequest(Request request) {
+       public boolean isPrefixPage() {
+               return false;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public Response handleRequest(FreenetRequest request, Response response) throws IOException {
                String imageId = request.getHttpRequest().getParam("image");
                TemporaryImage temporaryImage = webInterface.getCore().getTemporaryImage(imageId);
                if (temporaryImage == null) {
-                       return new Response(404, "Not found.", "text/plain; charset=utf-8", "");
+                       return response.setStatusCode(404).setStatusText("Not found.").setContentType("text/html; charset=utf-8");
                }
-               return new Response(200, "OK", temporaryImage.getMimeType(), temporaryImage.getImageData()).setHeader("Content-Disposition", "attachment; filename=" + temporaryImage.getId() + "." + temporaryImage.getMimeType().substring(temporaryImage.getMimeType().lastIndexOf("/") + 1));
+               String contentType= temporaryImage.getMimeType();
+               return response.setStatusCode(200).setStatusText("OK").setContentType(contentType).addHeader("Content-Disposition", "attachment; filename=" + temporaryImage.getId() + "." + contentType.substring(contentType.lastIndexOf('/') + 1)).write(temporaryImage.getImageData());
        }
 
 }
index 778b429..ed31283 100644 (file)
@@ -20,6 +20,7 @@ package net.pterodactylus.sone.web;
 import net.pterodactylus.sone.data.Album;
 import net.pterodactylus.sone.data.Image;
 import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
 
@@ -50,7 +51,7 @@ public class ImageBrowserPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                String albumId = request.getHttpRequest().getParam("album", null);
                if (albumId != null) {
index e675311..356b4ab 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * FreenetSone - IndexPage.java - Copyright © 2010 David Roden
+ * Sone - IndexPage.java - Copyright © 2010 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -23,7 +23,10 @@ import java.util.List;
 
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.notify.ListNotificationFilters;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.collection.Pagination;
+import net.pterodactylus.util.filter.Filter;
 import net.pterodactylus.util.filter.Filters;
 import net.pterodactylus.util.number.Numbers;
 import net.pterodactylus.util.template.Template;
@@ -55,9 +58,9 @@ public class IndexPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
-               Sone currentSone = getCurrentSone(request.getToadletContext());
+               final Sone currentSone = getCurrentSone(request.getToadletContext());
                List<Post> allPosts = new ArrayList<Post>();
                allPosts.addAll(currentSone.getPosts());
                for (String friendSoneId : currentSone.getFriends()) {
@@ -73,6 +76,13 @@ public class IndexPage extends SoneTemplatePage {
                                }
                        }
                }
+               allPosts = Filters.filteredList(allPosts, new Filter<Post>() {
+
+                       @Override
+                       public boolean filterObject(Post post) {
+                               return ListNotificationFilters.isPostVisible(currentSone, post);
+                       }
+               });
                allPosts = Filters.filteredList(allPosts, Post.FUTURE_POSTS_FILTER);
                Collections.sort(allPosts, Post.TIME_COMPARATOR);
                Pagination<Post> pagination = new Pagination<Post>(allPosts, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("page"), 0));
index 7c8f9ad..e5acad5 100644 (file)
@@ -22,7 +22,10 @@ import java.util.Collections;
 import java.util.List;
 
 import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.collection.Pagination;
+import net.pterodactylus.util.collection.ReverseComparator;
+import net.pterodactylus.util.filter.Filter;
 import net.pterodactylus.util.filter.Filters;
 import net.pterodactylus.util.number.Numbers;
 import net.pterodactylus.util.template.Template;
@@ -55,10 +58,52 @@ public class KnownSonesPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
+               String sortField = request.getHttpRequest().getParam("sort");
+               String sortOrder = request.getHttpRequest().getParam("order");
+               String followedSones = request.getHttpRequest().getParam("followedSones");
+               templateContext.set("sort", (sortField != null) ? sortField : "name");
+               templateContext.set("order", (sortOrder != null) ? sortOrder : "asc");
+               templateContext.set("followedSones", followedSones);
+               final Sone currentSone = getCurrentSone(request.getToadletContext(), false);
                List<Sone> knownSones = Filters.filteredList(new ArrayList<Sone>(webInterface.getCore().getSones()), Sone.EMPTY_SONE_FILTER);
-               Collections.sort(knownSones, Sone.NICE_NAME_COMPARATOR);
+               if ((currentSone != null) && "show-only".equals(followedSones)) {
+                       knownSones = Filters.filteredList(knownSones, new Filter<Sone>() {
+
+                               @Override
+                               public boolean filterObject(Sone sone) {
+                                       return currentSone.hasFriend(sone.getId());
+                               }
+                       });
+               } else if ((currentSone != null) && "hide".equals(followedSones)) {
+                       knownSones = Filters.filteredList(knownSones, new Filter<Sone>() {
+
+                               @Override
+                               public boolean filterObject(Sone sone) {
+                                       return !currentSone.hasFriend(sone.getId());
+                               }
+                       });
+               }
+               if ("activity".equals(sortField)) {
+                       if ("asc".equals(sortOrder)) {
+                               Collections.sort(knownSones, new ReverseComparator<Sone>(Sone.LAST_ACTIVITY_COMPARATOR));
+                       } else {
+                               Collections.sort(knownSones, Sone.LAST_ACTIVITY_COMPARATOR);
+                       }
+               } else if ("posts".equals(sortField)) {
+                       if ("asc".equals(sortOrder)) {
+                               Collections.sort(knownSones, new ReverseComparator<Sone>(Sone.POST_COUNT_COMPARATOR));
+                       } else {
+                               Collections.sort(knownSones, Sone.POST_COUNT_COMPARATOR);
+                       }
+               } else {
+                       if ("desc".equals(sortOrder)) {
+                               Collections.sort(knownSones, new ReverseComparator<Sone>(Sone.NICE_NAME_COMPARATOR));
+                       } else {
+                               Collections.sort(knownSones, Sone.NICE_NAME_COMPARATOR);
+                       }
+               }
                Pagination<Sone> sonePagination = new Pagination<Sone>(knownSones, 25).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("page"), 0));
                templateContext.set("pagination", sonePagination);
                templateContext.set("knownSones", sonePagination.getItems());
index 73fdd3c..c5174c6 100644 (file)
@@ -19,9 +19,10 @@ package net.pterodactylus.sone.web;
 
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
 
 /**
  * Page that lets the user like a {@link Post}.
@@ -50,10 +51,10 @@ public class LikePage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                if (request.getMethod() == Method.POST) {
-                       String type=request.getHttpRequest().getPartAsStringFailsafe("type", 16);
+                       String type = request.getHttpRequest().getPartAsStringFailsafe("type", 16);
                        String id = request.getHttpRequest().getPartAsStringFailsafe(type, 36);
                        String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
                        Sone currentSone = getCurrentSone(request.getToadletContext());
index d09de66..a56d263 100644 (file)
@@ -18,6 +18,7 @@
 package net.pterodactylus.sone.web;
 
 import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
 
@@ -49,7 +50,7 @@ public class LockSonePage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                String soneId = request.getHttpRequest().getPartAsStringFailsafe("sone", 44);
                Sone sone = webInterface.getCore().getLocalSone(soneId, false);
index eaa8351..6f43b6f 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * FreenetSone - LoginPage.java - Copyright © 2010 David Roden
+ * Sone - LoginPage.java - Copyright © 2010 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
@@ -24,10 +24,11 @@ import java.util.logging.Logger;
 
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.freenet.wot.OwnIdentity;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.logging.Logging;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
 import freenet.clients.http.ToadletContext;
 
 /**
@@ -61,7 +62,7 @@ public class LoginPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                /* get all own identities. */
                List<Sone> localSones = new ArrayList<Sone>(webInterface.getCore().getLocalSones());
@@ -87,7 +88,7 @@ public class LoginPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected String getRedirectTarget(Request request) {
+       protected String getRedirectTarget(FreenetRequest request) {
                if (getCurrentSone(request.getToadletContext(), false) != null) {
                        return "index.html";
                }
@@ -103,6 +104,9 @@ public class LoginPage extends SoneTemplatePage {
         */
        @Override
        public boolean isEnabled(ToadletContext toadletContext) {
+               if (webInterface.getCore().getPreferences().isRequireFullAccess() && !toadletContext.isAllowedFullAccess()) {
+                       return false;
+               }
                return getCurrentSone(toadletContext, false) == null;
        }
 
index f388368..a7f769f 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * FreenetSone - LogoutPage.java - Copyright © 2010 David Roden
+ * Sone - LogoutPage.java - Copyright © 2010 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
@@ -17,6 +17,7 @@
 
 package net.pterodactylus.sone.web;
 
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
 import freenet.clients.http.ToadletContext;
@@ -46,7 +47,7 @@ public class LogoutPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                setCurrentSone(request.getToadletContext(), null);
                super.processTemplate(request, templateContext);
                throw new RedirectException("index.html");
@@ -57,6 +58,9 @@ public class LogoutPage extends SoneTemplatePage {
         */
        @Override
        public boolean isEnabled(ToadletContext toadletContext) {
+               if (webInterface.getCore().getPreferences().isRequireFullAccess() && !toadletContext.isAllowedFullAccess()) {
+                       return false;
+               }
                return (getCurrentSone(toadletContext, false) != null) && (webInterface.getCore().getLocalSones().size() != 1);
        }
 
index 4ecf6e0..987fa1f 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - MarkReadPage.java - Copyright © 2011 David Roden
+ * Sone - MarkAsKnownPage.java - Copyright © 2011 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
@@ -22,6 +22,7 @@ import java.util.StringTokenizer;
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Reply;
 import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
 
@@ -53,7 +54,7 @@ public class MarkAsKnownPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                String type = request.getHttpRequest().getPartAsStringFailsafe("type", 5);
                if (!type.equals("sone") && !type.equals("post") && !type.equals("reply")) {
index 39cacc6..75e73a0 100644 (file)
 
 package net.pterodactylus.sone.web;
 
+import java.util.ArrayList;
+import java.util.List;
+
 import net.pterodactylus.sone.core.Core.Preferences;
 import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.number.Numbers;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
 
 /**
  * This page lets the user edit the options of the Sone plugin.
@@ -51,47 +56,84 @@ public class OptionsPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                Preferences preferences = webInterface.getCore().getPreferences();
                Sone currentSone = webInterface.getCurrentSone(request.getToadletContext(), false);
                if (request.getMethod() == Method.POST) {
+                       List<String> fieldErrors = new ArrayList<String>();
                        if (currentSone != null) {
                                boolean autoFollow = request.getHttpRequest().isPartSet("auto-follow");
                                currentSone.getOptions().getBooleanOption("AutoFollow").set(autoFollow);
-                               webInterface.getCore().saveSone(currentSone);
+                               boolean enableSoneInsertNotifications = request.getHttpRequest().isPartSet("enable-sone-insert-notifications");
+                               currentSone.getOptions().getBooleanOption("EnableSoneInsertNotifications").set(enableSoneInsertNotifications);
+                               webInterface.getCore().touchConfiguration();
                        }
                        Integer insertionDelay = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("insertion-delay", 16));
-                       preferences.setInsertionDelay(insertionDelay);
+                       if (!preferences.validateInsertionDelay(insertionDelay)) {
+                               fieldErrors.add("insertion-delay");
+                       } else {
+                               preferences.setInsertionDelay(insertionDelay);
+                       }
                        Integer postsPerPage = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("posts-per-page", 4), null);
-                       preferences.setPostsPerPage(postsPerPage);
+                       if (!preferences.validatePostsPerPage(postsPerPage)) {
+                               fieldErrors.add("posts-per-page");
+                       } else {
+                               preferences.setPostsPerPage(postsPerPage);
+                       }
+                       Integer charactersPerPost = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("characters-per-post", 10), null);
+                       if (!preferences.validateCharactersPerPost(charactersPerPost)) {
+                               fieldErrors.add("characters-per-post");
+                       } else {
+                               preferences.setCharactersPerPost(charactersPerPost);
+                       }
+                       boolean requireFullAccess = request.getHttpRequest().isPartSet("require-full-access");
+                       preferences.setRequireFullAccess(requireFullAccess);
                        Integer positiveTrust = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("positive-trust", 3));
-                       preferences.setPositiveTrust(positiveTrust);
+                       if (!preferences.validatePositiveTrust(positiveTrust)) {
+                               fieldErrors.add("positive-trust");
+                       } else {
+                               preferences.setPositiveTrust(positiveTrust);
+                       }
                        Integer negativeTrust = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("negative-trust", 4));
-                       preferences.setNegativeTrust(negativeTrust);
+                       if (!preferences.validateNegativeTrust(negativeTrust)) {
+                               fieldErrors.add("negative-trust");
+                       } else {
+                               preferences.setNegativeTrust(negativeTrust);
+                       }
                        String trustComment = request.getHttpRequest().getPartAsStringFailsafe("trust-comment", 256);
                        if (trustComment.trim().length() == 0) {
                                trustComment = null;
                        }
                        preferences.setTrustComment(trustComment);
-                       boolean soneRescueMode = Boolean.parseBoolean(request.getHttpRequest().getPartAsStringFailsafe("sone-rescue-mode", 5));
-                       preferences.setSoneRescueMode(soneRescueMode);
+                       boolean fcpInterfaceActive = request.getHttpRequest().isPartSet("fcp-interface-active");
+                       preferences.setFcpInterfaceActive(fcpInterfaceActive);
+                       Integer fcpFullAccessRequiredInteger = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("fcp-full-access-required", 1), preferences.getFcpFullAccessRequired().ordinal());
+                       FullAccessRequired fcpFullAccessRequired = FullAccessRequired.values()[fcpFullAccessRequiredInteger];
+                       preferences.setFcpFullAccessRequired(fcpFullAccessRequired);
                        boolean clearOnNextRestart = Boolean.parseBoolean(request.getHttpRequest().getPartAsStringFailsafe("clear-on-next-restart", 5));
                        preferences.setClearOnNextRestart(clearOnNextRestart);
                        boolean reallyClearOnNextRestart = Boolean.parseBoolean(request.getHttpRequest().getPartAsStringFailsafe("really-clear-on-next-restart", 5));
                        preferences.setReallyClearOnNextRestart(reallyClearOnNextRestart);
-                       webInterface.getCore().saveConfiguration();
-                       throw new RedirectException(getPath());
+                       webInterface.getCore().touchConfiguration();
+                       if (fieldErrors.isEmpty()) {
+                               throw new RedirectException(getPath());
+                       }
+                       templateContext.set("fieldErrors", fieldErrors);
                }
                if (currentSone != null) {
                        templateContext.set("auto-follow", currentSone.getOptions().getBooleanOption("AutoFollow").get());
+                       templateContext.set("enable-sone-insert-notifications", currentSone.getOptions().getBooleanOption("EnableSoneInsertNotifications").get());
                }
                templateContext.set("insertion-delay", preferences.getInsertionDelay());
                templateContext.set("posts-per-page", preferences.getPostsPerPage());
+               templateContext.set("characters-per-post", preferences.getCharactersPerPost());
+               templateContext.set("require-full-access", preferences.isRequireFullAccess());
                templateContext.set("positive-trust", preferences.getPositiveTrust());
                templateContext.set("negative-trust", preferences.getNegativeTrust());
                templateContext.set("trust-comment", preferences.getTrustComment());
-               templateContext.set("sone-rescue-mode", preferences.isSoneRescueMode());
+               templateContext.set("fcp-interface-active", preferences.isFcpInterfaceActive());
+               templateContext.set("fcp-full-access-required", preferences.getFcpFullAccessRequired().ordinal());
                templateContext.set("clear-on-next-restart", preferences.isClearOnNextRestart());
                templateContext.set("really-clear-on-next-restart", preferences.isReallyClearOnNextRestart());
        }
diff --git a/src/main/java/net/pterodactylus/sone/web/RescuePage.java b/src/main/java/net/pterodactylus/sone/web/RescuePage.java
new file mode 100644 (file)
index 0000000..f653615
--- /dev/null
@@ -0,0 +1,73 @@
+/*
+ * Sone - RescuePage.java - Copyright © 2011 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.web;
+
+import net.pterodactylus.sone.core.SoneRescuer;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.page.FreenetRequest;
+import net.pterodactylus.util.number.Numbers;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
+
+/**
+ * Page that lets the user control the rescue mode for a Sone.
+ *
+ * @see SoneRescuer
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class RescuePage extends SoneTemplatePage {
+
+       /**
+        * Creates a new rescue page.
+        *
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public RescuePage(Template template, WebInterface webInterface) {
+               super("rescue.html", template, "Page.Rescue.Title", webInterface, true);
+       }
+
+       //
+       // SONETEMPLATEPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
+               super.processTemplate(request, templateContext);
+               Sone currentSone = getCurrentSone(request.getToadletContext(), false);
+               SoneRescuer soneRescuer = webInterface.getCore().getSoneRescuer(currentSone);
+               if (request.getMethod() == Method.POST) {
+                       if ("true".equals(request.getHttpRequest().getPartAsStringFailsafe("fetch", 4))) {
+                               long edition = Numbers.safeParseLong(request.getHttpRequest().getPartAsStringFailsafe("edition", 8), -1L);
+                               if (edition > -1) {
+                                       soneRescuer.setEdition(edition);
+                               }
+                               soneRescuer.startNextFetch();
+                       }
+                       throw new RedirectException("rescue.html");
+               }
+               templateContext.set("soneRescuer", soneRescuer);
+       }
+
+}
index 1d1aa36..42f0a82 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - OptionsPage.java - Copyright © 2010 David Roden
+ * Sone - SearchPage.java - Copyright © 2010 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
@@ -24,17 +24,28 @@ import java.util.Comparator;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
 
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Profile;
 import net.pterodactylus.sone.data.Profile.Field;
 import net.pterodactylus.sone.data.Reply;
 import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.util.collection.Converter;
-import net.pterodactylus.util.collection.Converters;
+import net.pterodactylus.sone.web.page.FreenetRequest;
+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.Mapper;
+import net.pterodactylus.util.collection.Mappers;
 import net.pterodactylus.util.collection.Pagination;
+import net.pterodactylus.util.collection.TimedMap;
 import net.pterodactylus.util.filter.Filter;
 import net.pterodactylus.util.filter.Filters;
+import net.pterodactylus.util.logging.Logging;
 import net.pterodactylus.util.number.Numbers;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
@@ -49,6 +60,23 @@ import net.pterodactylus.util.text.TextException;
  */
 public class SearchPage extends SoneTemplatePage {
 
+       /** The logger. */
+       private static final Logger logger = Logging.getLogger(SearchPage.class);
+
+       /** Short-term cache. */
+       private final Cache<List<Phrase>, Set<Hit<Post>>> hitCache = new MemoryCache<List<Phrase>, Set<Hit<Post>>>(new ValueRetriever<List<Phrase>, Set<Hit<Post>>>() {
+
+               @SuppressWarnings("synthetic-access")
+               public CacheItem<Set<Hit<Post>>> retrieve(List<Phrase> phrases) throws CacheException {
+                       Set<Post> posts = new HashSet<Post>();
+                       for (Sone sone : webInterface.getCore().getSones()) {
+                               posts.addAll(sone.getPosts());
+                       }
+                       return new DefaultCacheItem<Set<Hit<Post>>>(getHits(Filters.filteredSet(posts, Post.FUTURE_POSTS_FILTER), phrases, new PostStringGenerator()));
+               }
+
+       }, new TimedMap<List<Phrase>, CacheItem<Set<Hit<Post>>>>(300000));
+
        /**
         * Creates a new search page.
         *
@@ -69,7 +97,7 @@ public class SearchPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                String query = request.getHttpRequest().getParam("query").trim();
                if (query.length() == 0) {
@@ -77,16 +105,21 @@ public class SearchPage extends SoneTemplatePage {
                }
 
                List<Phrase> phrases = parseSearchPhrases(query);
+               if (phrases.isEmpty()) {
+                       throw new RedirectException("index.html");
+               }
 
                Set<Sone> sones = webInterface.getCore().getSones();
                Set<Hit<Sone>> soneHits = getHits(sones, phrases, SoneStringGenerator.COMPLETE_GENERATOR);
 
-               Set<Post> posts = new HashSet<Post>();
-               for (Sone sone : sones) {
-                       posts.addAll(sone.getPosts());
+               Set<Hit<Post>> postHits;
+               try {
+                       postHits = hitCache.get(phrases);
+               } catch (CacheException ce1) {
+                       /* should never happen. */
+                       logger.log(Level.SEVERE, "Could not get search results from cache!", ce1);
+                       postHits = Collections.emptySet();
                }
-               @SuppressWarnings("synthetic-access")
-               Set<Hit<Post>> postHits = getHits(Filters.filteredSet(posts, Post.FUTURE_POSTS_FILTER), phrases, new PostStringGenerator());
 
                /* now filter. */
                soneHits = Filters.filteredSet(soneHits, Hit.POSITIVE_FILTER);
@@ -99,8 +132,8 @@ public class SearchPage extends SoneTemplatePage {
                Collections.sort(sortedPostHits, Hit.DESCENDING_COMPARATOR);
 
                /* extract Sones and posts. */
-               List<Sone> resultSones = Converters.convertList(sortedSoneHits, new HitConverter<Sone>());
-               List<Post> resultPosts = Converters.convertList(sortedPostHits, new HitConverter<Post>());
+               List<Sone> resultSones = Mappers.mappedList(sortedSoneHits, new HitMapper<Sone>());
+               List<Post> resultPosts = Mappers.mappedList(sortedPostHits, new HitMapper<Post>());
 
                /* pagination. */
                Pagination<Sone> sonePagination = new Pagination<Sone>(resultSones, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("sonePage"), 0));
@@ -137,7 +170,7 @@ public class SearchPage extends SoneTemplatePage {
                Set<Hit<T>> hits = new HashSet<Hit<T>>();
                for (T object : objects) {
                        String objectString = stringGenerator.generateString(object);
-                       int score = calculateScore(phrases, objectString);
+                       double score = calculateScore(phrases, objectString);
                        hits.add(new Hit<T>(object, score));
                }
                return hits;
@@ -166,11 +199,20 @@ public class SearchPage extends SoneTemplatePage {
                List<Phrase> phrases = new ArrayList<Phrase>();
                for (String phrase : parsedPhrases) {
                        if (phrase.startsWith("+")) {
-                               phrases.add(new Phrase(phrase.substring(1), Phrase.Optionality.REQUIRED));
+                               if (phrase.length() > 1) {
+                                       phrases.add(new Phrase(phrase.substring(1), Phrase.Optionality.REQUIRED));
+                               } else {
+                                       phrases.add(new Phrase("+", Phrase.Optionality.OPTIONAL));
+                               }
                        } else if (phrase.startsWith("-")) {
-                               phrases.add(new Phrase(phrase.substring(1), Phrase.Optionality.FORBIDDEN));
+                               if (phrase.length() > 1) {
+                                       phrases.add(new Phrase(phrase.substring(1), Phrase.Optionality.FORBIDDEN));
+                               } else {
+                                       phrases.add(new Phrase("-", Phrase.Optionality.OPTIONAL));
+                               }
+                       } else {
+                               phrases.add(new Phrase(phrase, Phrase.Optionality.OPTIONAL));
                        }
-                       phrases.add(new Phrase(phrase, Phrase.Optionality.OPTIONAL));
                }
                return phrases;
        }
@@ -185,9 +227,10 @@ public class SearchPage extends SoneTemplatePage {
         *            The expression to search
         * @return The score of the expression
         */
-       private int calculateScore(List<Phrase> phrases, String expression) {
-               int optionalHits = 0;
-               int requiredHits = 0;
+       private double calculateScore(List<Phrase> phrases, String expression) {
+               logger.log(Level.FINEST, "Calculating Score for “%s”…", expression);
+               double optionalHits = 0;
+               double requiredHits = 0;
                int forbiddenHits = 0;
                int requiredPhrases = 0;
                for (Phrase phrase : phrases) {
@@ -197,22 +240,26 @@ public class SearchPage extends SoneTemplatePage {
                        }
                        int matches = 0;
                        int index = 0;
+                       double score = 0;
                        while (index < expression.length()) {
                                int position = expression.toLowerCase().indexOf(phraseString, index);
                                if (position == -1) {
                                        break;
                                }
+                               score += Math.pow(1 - position / (double) expression.length(), 2);
                                index = position + phraseString.length();
+                               logger.log(Level.FINEST, "Got hit at position %d.", position);
                                ++matches;
                        }
+                       logger.log(Level.FINEST, "Score: %f", score);
                        if (matches == 0) {
                                continue;
                        }
                        if (phrase.getOptionality() == Phrase.Optionality.REQUIRED) {
-                               requiredHits += matches;
+                               requiredHits += score;
                        }
                        if (phrase.getOptionality() == Phrase.Optionality.OPTIONAL) {
-                               optionalHits += matches;
+                               optionalHits += score;
                        }
                        if (phrase.getOptionality() == Phrase.Optionality.FORBIDDEN) {
                                forbiddenHits += matches;
@@ -390,6 +437,31 @@ public class SearchPage extends SoneTemplatePage {
                        return optionality;
                }
 
+               //
+               // OBJECT METHODS
+               //
+
+               /**
+                * {@inheritDoc}
+                */
+               @Override
+               public int hashCode() {
+                       return phrase.hashCode() ^ ((optionality == Optionality.FORBIDDEN) ? (0xaaaaaaaa) : ((optionality == Optionality.REQUIRED) ? 0x55555555 : 0));
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               @Override
+               public boolean equals(Object object) {
+                       if (!(object instanceof Phrase)) {
+                               return false;
+                       }
+                       @SuppressWarnings("hiding")
+                       Phrase phrase = (Phrase) object;
+                       return (this.optionality == phrase.optionality) && this.phrase.equals(phrase.phrase);
+               }
+
        }
 
        /**
@@ -418,7 +490,7 @@ public class SearchPage extends SoneTemplatePage {
 
                        @Override
                        public int compare(Hit<?> leftHit, Hit<?> rightHit) {
-                               return rightHit.getScore() - leftHit.getScore();
+                               return (rightHit.getScore() < leftHit.getScore()) ? -1 : ((rightHit.getScore() > leftHit.getScore()) ? 1 : 0);
                        }
 
                };
@@ -427,7 +499,7 @@ public class SearchPage extends SoneTemplatePage {
                private final T object;
 
                /** The score of the object. */
-               private final int score;
+               private final double score;
 
                /**
                 * Creates a new hit.
@@ -437,7 +509,7 @@ public class SearchPage extends SoneTemplatePage {
                 * @param score
                 *            The score of the object
                 */
-               public Hit(T object, int score) {
+               public Hit(T object, double score) {
                        this.object = object;
                        this.score = score;
                }
@@ -456,7 +528,7 @@ public class SearchPage extends SoneTemplatePage {
                 *
                 * @return The score of the object
                 */
-               public int getScore() {
+               public double getScore() {
                        return score;
                }
 
@@ -469,13 +541,13 @@ public class SearchPage extends SoneTemplatePage {
         *            The type of the object to extract
         * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
         */
-       public static class HitConverter<T> implements Converter<Hit<T>, T> {
+       public static class HitMapper<T> implements Mapper<Hit<T>, T> {
 
                /**
                 * {@inheritDoc}
                 */
                @Override
-               public T convert(Hit<T> input) {
+               public T map(Hit<T> input) {
                        return input.getObject();
                }
 
index d6c41c1..095db9e 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Freetalk - FreetalkTemplatePage.java - Copyright © 2010 David Roden
+ * Sone - SoneTemplatePage.java - Copyright © 2010 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
@@ -26,7 +26,8 @@ import java.util.Map;
 
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.main.SonePlugin;
-import net.pterodactylus.sone.web.page.Page;
+import net.pterodactylus.sone.notify.ListNotificationFilters;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.sone.web.page.FreenetTemplatePage;
 import net.pterodactylus.util.collection.ListBuilder;
 import net.pterodactylus.util.collection.MapBuilder;
@@ -37,7 +38,7 @@ import freenet.clients.http.ToadletContext;
 import freenet.support.api.HTTPRequest;
 
 /**
- * Base page for the Freetalk web interface.
+ * Base page for the Sone web interface.
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
@@ -53,8 +54,8 @@ public class SoneTemplatePage extends FreenetTemplatePage {
        private final boolean requireLogin;
 
        /**
-        * Creates a new template page for Freetalk that does not require the user
-        * to be logged in.
+        * Creates a new template page for Sone that does not require the user to be
+        * logged in.
         *
         * @param path
         *            The path of the page
@@ -68,8 +69,8 @@ public class SoneTemplatePage extends FreenetTemplatePage {
        }
 
        /**
-        * Creates a new template page for Freetalk that does not require the user
-        * to be logged in.
+        * Creates a new template page for Sone that does not require the user to be
+        * logged in.
         *
         * @param path
         *            The path of the page
@@ -85,7 +86,7 @@ public class SoneTemplatePage extends FreenetTemplatePage {
        }
 
        /**
-        * Creates a new template page for Freetalk.
+        * Creates a new template page for Sone.
         *
         * @param path
         *            The path of the page
@@ -101,7 +102,7 @@ public class SoneTemplatePage extends FreenetTemplatePage {
        }
 
        /**
-        * Creates a new template page for Freetalk.
+        * Creates a new template page for Sone.
         *
         * @param path
         *            The path of the page
@@ -119,7 +120,6 @@ public class SoneTemplatePage extends FreenetTemplatePage {
                this.pageTitleKey = pageTitleKey;
                this.webInterface = webInterface;
                this.requireLogin = requireLogin;
-               template.getInitialContext().set("webInterface", webInterface);
        }
 
        //
@@ -202,7 +202,7 @@ public class SoneTemplatePage extends FreenetTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected String getPageTitle(Request request) {
+       protected String getPageTitle(FreenetRequest request) {
                if (pageTitleKey != null) {
                        return webInterface.getL10n().getString(pageTitleKey);
                }
@@ -213,7 +213,7 @@ public class SoneTemplatePage extends FreenetTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected List<Map<String, String>> getAdditionalLinkNodes(Request request) {
+       protected List<Map<String, String>> getAdditionalLinkNodes(FreenetRequest request) {
                return new ListBuilder<Map<String, String>>().add(new MapBuilder<String, String>().put("rel", "search").put("type", "application/opensearchdescription+xml").put("title", "Sone").put("href", "http://" + request.getHttpRequest().getHeader("host") + "/Sone/OpenSearch.xml").get()).get();
        }
 
@@ -247,9 +247,11 @@ public class SoneTemplatePage extends FreenetTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
-               templateContext.set("currentSone", getCurrentSone(request.getToadletContext(), false));
+               Sone currentSone = getCurrentSone(request.getToadletContext(), false);
+               templateContext.set("core", webInterface.getCore());
+               templateContext.set("currentSone", currentSone);
                templateContext.set("localSones", webInterface.getCore().getLocalSones());
                templateContext.set("request", request);
                templateContext.set("currentVersion", SonePlugin.VERSION);
@@ -257,13 +259,14 @@ public class SoneTemplatePage extends FreenetTemplatePage {
                templateContext.set("latestEdition", webInterface.getCore().getUpdateChecker().getLatestEdition());
                templateContext.set("latestVersion", webInterface.getCore().getUpdateChecker().getLatestVersion());
                templateContext.set("latestVersionTime", webInterface.getCore().getUpdateChecker().getLatestVersionDate());
+               templateContext.set("notifications", ListNotificationFilters.filterNotifications(webInterface.getNotifications().getNotifications(), currentSone));
        }
 
        /**
         * {@inheritDoc}
         */
        @Override
-       protected String getRedirectTarget(Page.Request request) {
+       protected String getRedirectTarget(FreenetRequest request) {
                if (requiresLogin() && (getCurrentSone(request.getToadletContext(), false) == null)) {
                        HTTPRequest httpRequest = request.getHttpRequest();
                        String originalUrl = httpRequest.getPath();
@@ -293,7 +296,18 @@ public class SoneTemplatePage extends FreenetTemplatePage {
         * {@inheritDoc}
         */
        @Override
+       protected boolean isFullAccessOnly() {
+               return webInterface.getCore().getPreferences().isRequireFullAccess();
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
        public boolean isEnabled(ToadletContext toadletContext) {
+               if (webInterface.getCore().getPreferences().isRequireFullAccess() && !toadletContext.isAllowedFullAccess()) {
+                       return false;
+               }
                if (requiresLogin()) {
                        return getCurrentSone(toadletContext, false) != null;
                }
index b0dadef..f2cf92e 100644 (file)
@@ -19,9 +19,10 @@ package net.pterodactylus.sone.web;
 
 import net.pterodactylus.sone.core.Core;
 import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
 
 /**
  * Page that lets the user trust another Sone. This will assign a configurable
@@ -52,7 +53,7 @@ public class TrustPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                if (request.getMethod() == Method.POST) {
                        String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
index 85f46f0..def3bf4 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - BookmarkPage.java - Copyright © 2011 David Roden
+ * Sone - UnbookmarkPage.java - Copyright © 2011 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
@@ -20,9 +20,10 @@ package net.pterodactylus.sone.web;
 import java.util.Set;
 
 import net.pterodactylus.sone.data.Post;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
 
 /**
  * Page that lets the user unbookmark a post.
@@ -49,7 +50,7 @@ public class UnbookmarkPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                if (request.getMethod() == Method.POST) {
                        String id = request.getHttpRequest().getPartAsStringFailsafe("post", 36);
index e026a9a..d8e53ce 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - FollowSonePage.java - Copyright © 2010 David Roden
+ * Sone - UnfollowSonePage.java - Copyright © 2010 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
 package net.pterodactylus.sone.web;
 
 import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
 
 /**
  * This page lets the user unfollow another Sone.
@@ -47,14 +48,16 @@ public class UnfollowSonePage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                if (request.getMethod() == Method.POST) {
-                       String soneId = request.getHttpRequest().getPartAsStringFailsafe("sone", 44);
                        String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
                        Sone currentSone = getCurrentSone(request.getToadletContext());
-                       currentSone.removeFriend(soneId);
-                       webInterface.getCore().saveSone(currentSone);
+                       String soneIds = request.getHttpRequest().getPartAsStringFailsafe("sone", 2000);
+                       for (String soneId : soneIds.split("[ ,]+")) {
+                               currentSone.removeFriend(soneId);
+                       }
+                       webInterface.getCore().touchConfiguration();
                        throw new RedirectException(returnPage);
                }
        }
index 24ff3ca..f47c4f2 100644 (file)
@@ -19,9 +19,10 @@ package net.pterodactylus.sone.web;
 
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
 
 /**
  * Page that lets the user unlike a {@link Post}.
@@ -50,7 +51,7 @@ public class UnlikePage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                if (request.getMethod() == Method.POST) {
                        String type = request.getHttpRequest().getPartAsStringFailsafe("type", 16);
index 5408f20..f527558 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - LockSonePage.java - Copyright © 2010 David Roden
+ * Sone - UnlockSonePage.java - Copyright © 2010 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
@@ -18,6 +18,7 @@
 package net.pterodactylus.sone.web;
 
 import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
 
@@ -48,7 +49,7 @@ public class UnlockSonePage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                String soneId = request.getHttpRequest().getPartAsStringFailsafe("sone", 44);
                Sone sone = webInterface.getCore().getLocalSone(soneId, false);
index 0e7e198..d711525 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - TrustPage.java - Copyright © 2011 David Roden
+ * Sone - UntrustPage.java - Copyright © 2011 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
@@ -19,9 +19,10 @@ package net.pterodactylus.sone.web;
 
 import net.pterodactylus.sone.core.Core;
 import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
 
 /**
  * Page that lets the user untrust another Sone. This will remove all trust
@@ -52,7 +53,7 @@ public class UntrustPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                if (request.getMethod() == Method.POST) {
                        String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
index 4d98179..e0aa1e1 100644 (file)
@@ -33,12 +33,13 @@ import javax.imageio.stream.ImageInputStream;
 import net.pterodactylus.sone.data.Album;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.data.TemporaryImage;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.io.Closer;
 import net.pterodactylus.util.io.StreamCopier;
 import net.pterodactylus.util.logging.Logging;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
 import freenet.support.api.Bucket;
 import freenet.support.api.HTTPUploadedFile;
 
@@ -72,7 +73,7 @@ public class UploadImagePage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                if (request.getMethod() == Method.POST) {
                        Sone currentSone = getCurrentSone(request.getToadletContext());
index b62e19c..4cc9864 100644 (file)
@@ -19,6 +19,7 @@ package net.pterodactylus.sone.web;
 
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.template.SoneAccessor;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
 
@@ -49,7 +50,7 @@ public class ViewPostPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected String getPageTitle(Request request) {
+       protected String getPageTitle(FreenetRequest request) {
                String postId = request.getHttpRequest().getParam("post");
                Post post = webInterface.getCore().getPost(postId, false);
                String title = "";
@@ -65,7 +66,7 @@ public class ViewPostPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                String postId = request.getHttpRequest().getParam("post");
                boolean raw = request.getHttpRequest().getParam("raw").equals("true");
index 14c4f94..5d463c0 100644 (file)
@@ -29,6 +29,7 @@ import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Reply;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.template.SoneAccessor;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.collection.Pagination;
 import net.pterodactylus.util.number.Numbers;
 import net.pterodactylus.util.template.Template;
@@ -61,7 +62,7 @@ public class ViewSonePage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected String getPageTitle(Request request) {
+       protected String getPageTitle(FreenetRequest request) {
                String soneId = request.getHttpRequest().getParam("sone");
                Sone sone = webInterface.getCore().getSone(soneId, false);
                if ((sone != null) && (sone.getTime() > 0)) {
@@ -75,11 +76,15 @@ public class ViewSonePage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                String soneId = request.getHttpRequest().getParam("sone");
                Sone sone = webInterface.getCore().getSone(soneId, false);
                templateContext.set("sone", sone);
+               templateContext.set("soneId", soneId);
+               if (sone == null) {
+                       return;
+               }
                List<Post> sonePosts = sone.getPosts();
                sonePosts.addAll(webInterface.getCore().getDirectedPosts(sone));
                Collections.sort(sonePosts, Post.TIME_COMPARATOR);
index f5b111f..6ea8957 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * FreenetSone - WebInterface.java - Copyright © 2010 David Roden
+ * Sone - WebInterface.java - Copyright © 2010 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
@@ -17,6 +17,7 @@
 
 package net.pterodactylus.sone.web;
 
+import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.Reader;
@@ -53,7 +54,6 @@ import net.pterodactylus.sone.template.HttpRequestAccessor;
 import net.pterodactylus.sone.template.IdentityAccessor;
 import net.pterodactylus.sone.template.ImageLinkFilter;
 import net.pterodactylus.sone.template.JavascriptFilter;
-import net.pterodactylus.sone.template.NotificationManagerAccessor;
 import net.pterodactylus.sone.template.ParserFilter;
 import net.pterodactylus.sone.template.PostAccessor;
 import net.pterodactylus.sone.template.ReplyAccessor;
@@ -62,7 +62,11 @@ import net.pterodactylus.sone.template.RequestChangeFilter;
 import net.pterodactylus.sone.template.SoneAccessor;
 import net.pterodactylus.sone.template.SubstringFilter;
 import net.pterodactylus.sone.template.TrustAccessor;
+import net.pterodactylus.sone.template.UniqueElementFilter;
 import net.pterodactylus.sone.template.UnknownDateFilter;
+import net.pterodactylus.sone.text.Part;
+import net.pterodactylus.sone.text.SonePart;
+import net.pterodactylus.sone.text.SoneTextParser;
 import net.pterodactylus.sone.web.ajax.BookmarkAjaxPage;
 import net.pterodactylus.sone.web.ajax.CreatePostAjaxPage;
 import net.pterodactylus.sone.web.ajax.CreateReplyAjaxPage;
@@ -74,6 +78,7 @@ import net.pterodactylus.sone.web.ajax.DistrustAjaxPage;
 import net.pterodactylus.sone.web.ajax.EditProfileFieldAjaxPage;
 import net.pterodactylus.sone.web.ajax.FollowSoneAjaxPage;
 import net.pterodactylus.sone.web.ajax.GetLikesAjaxPage;
+import net.pterodactylus.sone.web.ajax.GetNotificationAjaxPage;
 import net.pterodactylus.sone.web.ajax.GetPostAjaxPage;
 import net.pterodactylus.sone.web.ajax.GetReplyAjaxPage;
 import net.pterodactylus.sone.web.ajax.GetStatusAjaxPage;
@@ -89,22 +94,23 @@ import net.pterodactylus.sone.web.ajax.UnfollowSoneAjaxPage;
 import net.pterodactylus.sone.web.ajax.UnlikeAjaxPage;
 import net.pterodactylus.sone.web.ajax.UnlockSoneAjaxPage;
 import net.pterodactylus.sone.web.ajax.UntrustAjaxPage;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.sone.web.page.PageToadlet;
 import net.pterodactylus.sone.web.page.PageToadletFactory;
-import net.pterodactylus.sone.web.page.RedirectPage;
-import net.pterodactylus.sone.web.page.StaticPage;
-import net.pterodactylus.sone.web.page.TemplatePage;
 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.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.CollectionSortFilter;
+import net.pterodactylus.util.template.ContainsFilter;
 import net.pterodactylus.util.template.DateFilter;
 import net.pterodactylus.util.template.FormatFilter;
 import net.pterodactylus.util.template.HtmlFilter;
@@ -121,10 +127,13 @@ import net.pterodactylus.util.template.TemplateParser;
 import net.pterodactylus.util.template.XmlFilter;
 import net.pterodactylus.util.thread.Ticker;
 import net.pterodactylus.util.version.Version;
+import net.pterodactylus.util.web.RedirectPage;
+import net.pterodactylus.util.web.StaticPage;
+import net.pterodactylus.util.web.TemplatePage;
 import freenet.clients.http.SessionManager;
-import freenet.clients.http.SessionManager.Session;
 import freenet.clients.http.ToadletContainer;
 import freenet.clients.http.ToadletContext;
+import freenet.clients.http.SessionManager.Session;
 import freenet.l10n.BaseL10n;
 import freenet.support.api.HTTPRequest;
 
@@ -154,6 +163,9 @@ public class WebInterface implements CoreListener {
        /** The template context factory. */
        private final TemplateContextFactory templateContextFactory;
 
+       /** The Sone text parser. */
+       private final SoneTextParser soneTextParser;
+
        /** The “new Sone” notification. */
        private final ListNotification<Sone> newSoneNotification;
 
@@ -163,11 +175,17 @@ public class WebInterface implements CoreListener {
        /** The “new reply” notification. */
        private final ListNotification<Reply> newReplyNotification;
 
-       /** The “rescuing Sone” notification. */
-       private final ListNotification<Sone> rescuingSonesNotification;
+       /** The invisible “local post” notification. */
+       private final ListNotification<Post> localPostNotification;
+
+       /** The invisible “local reply” notification. */
+       private final ListNotification<Reply> localReplyNotification;
 
-       /** The “Sone rescued” notification. */
-       private final ListNotification<Sone> sonesRescuedNotification;
+       /** The “you have been mentioned” notification. */
+       private final ListNotification<Post> mentionNotification;
+
+       /** Notifications for sone inserts. */
+       private final Map<Sone, TemplateNotification> soneInsertNotifications = new HashMap<Sone, TemplateNotification>();
 
        /** Sone locked notification ticker objects. */
        private final Map<Sone, Object> lockedSonesTickerObjects = Collections.synchronizedMap(new HashMap<Sone, Object>());
@@ -197,6 +215,7 @@ public class WebInterface implements CoreListener {
        public WebInterface(SonePlugin sonePlugin) {
                this.sonePlugin = sonePlugin;
                formPassword = sonePlugin.pluginRespirator().getToadletContainer().getFormPassword();
+               soneTextParser = new SoneTextParser(getCore(), getCore());
 
                templateContextFactory = new TemplateContextFactory();
                templateContextFactory.addAccessor(Object.class, new ReflectionAccessor());
@@ -206,7 +225,6 @@ public class WebInterface implements CoreListener {
                templateContextFactory.addAccessor(Reply.class, new ReplyAccessor(getCore()));
                templateContextFactory.addAccessor(Album.class, new AlbumAccessor());
                templateContextFactory.addAccessor(Identity.class, new IdentityAccessor(getCore()));
-               templateContextFactory.addAccessor(NotificationManager.class, new NotificationManagerAccessor());
                templateContextFactory.addAccessor(Trust.class, new TrustAccessor());
                templateContextFactory.addAccessor(HTTPRequest.class, new HttpRequestAccessor());
                templateContextFactory.addFilter("date", new DateFilter());
@@ -220,14 +238,17 @@ public class WebInterface implements CoreListener {
                templateContextFactory.addFilter("match", new MatchFilter());
                templateContextFactory.addFilter("css", new CssClassNameFilter());
                templateContextFactory.addFilter("js", new JavascriptFilter());
-               templateContextFactory.addFilter("parse", new ParserFilter(getCore(), templateContextFactory));
+               templateContextFactory.addFilter("parse", new ParserFilter(getCore(), templateContextFactory, soneTextParser));
                templateContextFactory.addFilter("unknown", new UnknownDateFilter(getL10n(), "View.Sone.Text.UnknownDate"));
                templateContextFactory.addFilter("format", new FormatFilter());
                templateContextFactory.addFilter("sort", new CollectionSortFilter());
                templateContextFactory.addFilter("image-link", new ImageLinkFilter(templateContextFactory));
                templateContextFactory.addFilter("replyGroup", new ReplyGroupFilter());
+               templateContextFactory.addFilter("in", new ContainsFilter());
+               templateContextFactory.addFilter("unique", new UniqueElementFilter());
                templateContextFactory.addProvider(Provider.TEMPLATE_CONTEXT_PROVIDER);
                templateContextFactory.addProvider(new ClassPathTemplateProvider());
+               templateContextFactory.addTemplateObject("webInterface", this);
                templateContextFactory.addTemplateObject("formPassword", formPassword);
 
                /* create notifications. */
@@ -237,14 +258,17 @@ public class WebInterface implements CoreListener {
                Template newPostNotificationTemplate = TemplateParser.parse(createReader("/templates/notify/newPostNotification.html"));
                newPostNotification = new ListNotification<Post>("new-post-notification", "posts", newPostNotificationTemplate, false);
 
+               Template localPostNotificationTemplate = TemplateParser.parse(createReader("/templates/notify/newPostNotification.html"));
+               localPostNotification = new ListNotification<Post>("local-post-notification", "posts", localPostNotificationTemplate, false);
+
                Template newReplyNotificationTemplate = TemplateParser.parse(createReader("/templates/notify/newReplyNotification.html"));
-               newReplyNotification = new ListNotification<Reply>("new-replies-notification", "replies", newReplyNotificationTemplate, false);
+               newReplyNotification = new ListNotification<Reply>("new-reply-notification", "replies", newReplyNotificationTemplate, false);
 
-               Template rescuingSonesTemplate = TemplateParser.parse(createReader("/templates/notify/rescuingSonesNotification.html"));
-               rescuingSonesNotification = new ListNotification<Sone>("sones-being-rescued-notification", "sones", rescuingSonesTemplate);
+               Template localReplyNotificationTemplate = TemplateParser.parse(createReader("/templates/notify/newReplyNotification.html"));
+               localReplyNotification = new ListNotification<Reply>("local-reply-notification", "replies", localReplyNotificationTemplate, false);
 
-               Template sonesRescuedTemplate = TemplateParser.parse(createReader("/templates/notify/sonesRescuedNotification.html"));
-               sonesRescuedNotification = new ListNotification<Sone>("sones-rescued-notification", "sones", sonesRescuedTemplate);
+               Template mentionNotificationTemplate = TemplateParser.parse(createReader("/templates/notify/mentionNotification.html"));
+               mentionNotification = new ListNotification<Post>("mention-notification", "posts", mentionNotificationTemplate, false);
 
                Template lockedSonesTemplate = TemplateParser.parse(createReader("/templates/notify/lockedSonesNotification.html"));
                lockedSonesNotification = new ListNotification<Sone>("sones-locked-notification", "sones", lockedSonesTemplate);
@@ -427,7 +451,7 @@ public class WebInterface implements CoreListener {
         * @return The new posts
         */
        public Set<Post> getNewPosts() {
-               return new HashSet<Post>(newPostNotification.getElements());
+               return new SetBuilder<Post>().addAll(newPostNotification.getElements()).addAll(localPostNotification.getElements()).get();
        }
 
        /**
@@ -437,7 +461,7 @@ public class WebInterface implements CoreListener {
         * @return The new replies
         */
        public Set<Reply> getNewReplies() {
-               return new HashSet<Reply>(newReplyNotification.getElements());
+               return new SetBuilder<Reply>().addAll(newReplyNotification.getElements()).addAll(localReplyNotification.getElements()).get();
        }
 
        /**
@@ -566,6 +590,7 @@ public class WebInterface implements CoreListener {
                Template deleteImageTemplate = TemplateParser.parse(createReader("/templates/deleteImage.html"));
                Template noPermissionTemplate = TemplateParser.parse(createReader("/templates/noPermission.html"));
                Template optionsTemplate = TemplateParser.parse(createReader("/templates/options.html"));
+               Template rescueTemplate = TemplateParser.parse(createReader("/templates/rescue.html"));
                Template aboutTemplate = TemplateParser.parse(createReader("/templates/about.html"));
                Template invalidTemplate = TemplateParser.parse(createReader("/templates/invalid.html"));
                Template postTemplate = TemplateParser.parse(createReader("/templates/include/viewPost.html"));
@@ -573,7 +598,7 @@ public class WebInterface implements CoreListener {
                Template openSearchTemplate = TemplateParser.parse(createReader("/templates/xml/OpenSearch.xml"));
 
                PageToadletFactory pageToadletFactory = new PageToadletFactory(sonePlugin.pluginRespirator().getHLSimpleClient(), "/Sone/");
-               pageToadlets.add(pageToadletFactory.createPageToadlet(new RedirectPage("", "index.html")));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new RedirectPage<FreenetRequest>("", "index.html")));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new IndexPage(indexTemplate, this), "Index"));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new CreateSonePage(createSoneTemplate, this), "CreateSone"));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new KnownSonesPage(knownSonesTemplate, this), "KnownSones"));
@@ -611,17 +636,19 @@ public class WebInterface implements CoreListener {
                pageToadlets.add(pageToadletFactory.createPageToadlet(new LoginPage(loginTemplate, this), "Login"));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new LogoutPage(emptyTemplate, this), "Logout"));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new OptionsPage(optionsTemplate, this), "Options"));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new RescuePage(rescueTemplate, this), "Rescue"));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new AboutPage(aboutTemplate, this, SonePlugin.VERSION), "About"));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new SoneTemplatePage("noPermission.html", noPermissionTemplate, "Page.NoPermission.Title", this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new DismissNotificationPage(emptyTemplate, this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new SoneTemplatePage("invalid.html", invalidTemplate, "Page.Invalid.Title", this)));
-               pageToadlets.add(pageToadletFactory.createPageToadlet(new StaticPage("css/", "/static/css/", "text/css")));
-               pageToadlets.add(pageToadletFactory.createPageToadlet(new StaticPage("javascript/", "/static/javascript/", "text/javascript")));
-               pageToadlets.add(pageToadletFactory.createPageToadlet(new StaticPage("images/", "/static/images/", "image/png")));
-               pageToadlets.add(pageToadletFactory.createPageToadlet(new TemplatePage("OpenSearch.xml", "application/opensearchdescription+xml", templateContextFactory, openSearchTemplate)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new StaticPage<FreenetRequest>("css/", "/static/css/", "text/css")));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new StaticPage<FreenetRequest>("javascript/", "/static/javascript/", "text/javascript")));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new StaticPage<FreenetRequest>("images/", "/static/images/", "image/png")));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new TemplatePage<FreenetRequest>("OpenSearch.xml", "application/opensearchdescription+xml", templateContextFactory, openSearchTemplate)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new GetImagePage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new GetTranslationPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new GetStatusAjaxPage(this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new GetNotificationAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new DismissNotificationAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new CreatePostAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new CreateReplyAjaxPage(this)));
@@ -682,34 +709,59 @@ public class WebInterface implements CoreListener {
                try {
                        return new InputStreamReader(getClass().getResourceAsStream(resourceName), "UTF-8");
                } catch (UnsupportedEncodingException uee1) {
-                       System.out.println("  fail.");
                        return null;
                }
        }
 
-       //
-       // CORELISTENER METHODS
-       //
-
        /**
-        * {@inheritDoc}
+        * Returns all {@link Core#isLocalSone(Sone) local Sone}s that are
+        * referenced by {@link SonePart}s in the given text (after parsing it using
+        * {@link SoneTextParser}).
+        *
+        * @param text
+        *            The text to parse
+        * @return All mentioned local Sones
         */
-       @Override
-       public void rescuingSone(Sone sone) {
-               rescuingSonesNotification.add(sone);
-               notificationManager.addNotification(rescuingSonesNotification);
+       private Set<Sone> getMentionedSones(String text) {
+               /* we need no context to find mentioned Sones. */
+               Set<Sone> mentionedSones = new HashSet<Sone>();
+               try {
+                       for (Part part : soneTextParser.parse(null, new StringReader(text))) {
+                               if (part instanceof SonePart) {
+                                       mentionedSones.add(((SonePart) part).getSone());
+                               }
+                       }
+               } catch (IOException ioe1) {
+                       logger.log(Level.WARNING, "Could not parse post text: " + text, ioe1);
+               }
+               return Filters.filteredSet(mentionedSones, Sone.LOCAL_SONE_FILTER);
        }
 
        /**
-        * {@inheritDoc}
+        * Returns the Sone insert notification for the given Sone. If no
+        * notification for the given Sone exists, a new notification is created and
+        * cached.
+        *
+        * @param sone
+        *            The Sone to get the insert notification for
+        * @return The Sone insert notification
         */
-       @Override
-       public void rescuedSone(Sone sone) {
-               rescuingSonesNotification.remove(sone);
-               sonesRescuedNotification.add(sone);
-               notificationManager.addNotification(sonesRescuedNotification);
+       private TemplateNotification getSoneInsertNotification(Sone sone) {
+               synchronized (soneInsertNotifications) {
+                       TemplateNotification templateNotification = soneInsertNotifications.get(sone);
+                       if (templateNotification == null) {
+                               templateNotification = new TemplateNotification(TemplateParser.parse(createReader("/templates/notify/soneInsertNotification.html")));
+                               templateNotification.set("insertSone", sone);
+                               soneInsertNotifications.put(sone, templateNotification);
+                       }
+                       return templateNotification;
+               }
        }
 
+       //
+       // CORELISTENER METHODS
+       //
+
        /**
         * {@inheritDoc}
         */
@@ -726,9 +778,18 @@ public class WebInterface implements CoreListener {
         */
        @Override
        public void newPostFound(Post post) {
-               newPostNotification.add(post);
+               boolean isLocal = getCore().isLocalSone(post.getSone());
+               if (isLocal) {
+                       localPostNotification.add(post);
+               } else {
+                       newPostNotification.add(post);
+               }
                if (!hasFirstStartNotification()) {
-                       notificationManager.addNotification(newPostNotification);
+                       notificationManager.addNotification(isLocal ? localPostNotification : newPostNotification);
+                       if (!getMentionedSones(post.getText()).isEmpty() && !isLocal) {
+                               mentionNotification.add(post);
+                               notificationManager.addNotification(mentionNotification);
+                       }
                } else {
                        getCore().markPostKnown(post);
                }
@@ -739,12 +800,18 @@ public class WebInterface implements CoreListener {
         */
        @Override
        public void newReplyFound(Reply reply) {
-               if (reply.getPost().getSone() == null) {
-                       return;
+               boolean isLocal = getCore().isLocalSone(reply.getSone());
+               if (isLocal) {
+                       localReplyNotification.add(reply);
+               } else {
+                       newReplyNotification.add(reply);
                }
-               newReplyNotification.add(reply);
                if (!hasFirstStartNotification()) {
-                       notificationManager.addNotification(newReplyNotification);
+                       notificationManager.addNotification(isLocal ? localReplyNotification : newReplyNotification);
+                       if (!getMentionedSones(reply.getText()).isEmpty() && !isLocal && (reply.getPost().getSone() != null)) {
+                               mentionNotification.add(reply.getPost());
+                               notificationManager.addNotification(mentionNotification);
+                       }
                } else {
                        getCore().markReplyKnown(reply);
                }
@@ -764,6 +831,8 @@ public class WebInterface implements CoreListener {
        @Override
        public void markPostKnown(Post post) {
                newPostNotification.remove(post);
+               localPostNotification.remove(post);
+               mentionNotification.remove(post);
        }
 
        /**
@@ -772,6 +841,16 @@ public class WebInterface implements CoreListener {
        @Override
        public void markReplyKnown(Reply reply) {
                newReplyNotification.remove(reply);
+               localReplyNotification.remove(reply);
+               mentionNotification.remove(reply.getPost());
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public void soneRemoved(Sone sone) {
+               newSoneNotification.remove(sone);
        }
 
        /**
@@ -780,6 +859,7 @@ public class WebInterface implements CoreListener {
        @Override
        public void postRemoved(Post post) {
                newPostNotification.remove(post);
+               localPostNotification.remove(post);
        }
 
        /**
@@ -788,6 +868,7 @@ public class WebInterface implements CoreListener {
        @Override
        public void replyRemoved(Reply reply) {
                newReplyNotification.remove(reply);
+               localReplyNotification.remove(reply);
        }
 
        /**
@@ -821,6 +902,44 @@ public class WebInterface implements CoreListener {
         * {@inheritDoc}
         */
        @Override
+       public void soneInserting(Sone sone) {
+               TemplateNotification soneInsertNotification = getSoneInsertNotification(sone);
+               soneInsertNotification.set("soneStatus", "inserting");
+               if (sone.getOptions().getBooleanOption("EnableSoneInsertNotifications").get()) {
+                       notificationManager.addNotification(soneInsertNotification);
+               }
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public void soneInserted(Sone sone, long insertDuration) {
+               TemplateNotification soneInsertNotification = getSoneInsertNotification(sone);
+               soneInsertNotification.set("soneStatus", "inserted");
+               soneInsertNotification.set("insertDuration", insertDuration / 1000);
+               if (sone.getOptions().getBooleanOption("EnableSoneInsertNotifications").get()) {
+                       notificationManager.addNotification(soneInsertNotification);
+               }
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public void soneInsertAborted(Sone sone, Throwable cause) {
+               TemplateNotification soneInsertNotification = getSoneInsertNotification(sone);
+               soneInsertNotification.set("soneStatus", "insert-aborted");
+               soneInsertNotification.set("insert-error", cause);
+               if (sone.getOptions().getBooleanOption("EnableSoneInsertNotifications").get()) {
+                       notificationManager.addNotification(soneInsertNotification);
+               }
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
        public void updateFound(Version version, long releaseTime, long latestEdition) {
                newVersionNotification.getTemplateContext().set("latestVersion", version);
                newVersionNotification.getTemplateContext().set("latestEdition", latestEdition);
index 760bd34..0cf7b01 100644 (file)
@@ -18,6 +18,7 @@
 package net.pterodactylus.sone.web.ajax;
 
 import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.json.JsonObject;
 
 /**
@@ -45,7 +46,7 @@ public class BookmarkAjaxPage extends JsonPage {
         * {@inheritDoc}
         */
        @Override
-       protected JsonObject createJsonObject(Request request) {
+       protected JsonObject createJsonObject(FreenetRequest request) {
                String id = request.getHttpRequest().getParam("post", null);
                if ((id == null) || (id.length() == 0)) {
                        return createErrorJsonObject("invalid-post-id");
index eff7c41..c6f4455 100644 (file)
@@ -19,7 +19,9 @@ package net.pterodactylus.sone.web.ajax;
 
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.text.TextFilter;
 import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.json.JsonObject;
 
 /**
@@ -43,7 +45,7 @@ public class CreatePostAjaxPage extends JsonPage {
         * {@inheritDoc}
         */
        @Override
-       protected JsonObject createJsonObject(Request request) {
+       protected JsonObject createJsonObject(FreenetRequest request) {
                Sone sone = getCurrentSone(request.getToadletContext());
                if (sone == null) {
                        return createErrorJsonObject("auth-required");
@@ -59,6 +61,7 @@ public class CreatePostAjaxPage extends JsonPage {
                if ((text == null) || (text.trim().length() == 0)) {
                        return createErrorJsonObject("text-required");
                }
+               text = TextFilter.filter(request.getHttpRequest().getHeader("host"), text);
                Post newPost = webInterface.getCore().createPost(sender, recipient, text);
                return createSuccessJsonObject().put("postId", newPost.getId()).put("sone", sender.getId()).put("recipient", (newPost.getRecipient() != null) ? newPost.getRecipient().getId() : null);
        }
index 9ed960f..9f5c882 100644 (file)
@@ -20,7 +20,9 @@ package net.pterodactylus.sone.web.ajax;
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Reply;
 import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.text.TextFilter;
 import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.json.JsonObject;
 
 /**
@@ -48,7 +50,7 @@ public class CreateReplyAjaxPage extends JsonPage {
         * {@inheritDoc}
         */
        @Override
-       protected JsonObject createJsonObject(Request request) {
+       protected JsonObject createJsonObject(FreenetRequest request) {
                String postId = request.getHttpRequest().getParam("post");
                String text = request.getHttpRequest().getParam("text").trim();
                String senderId = request.getHttpRequest().getParam("sender");
@@ -60,6 +62,7 @@ public class CreateReplyAjaxPage extends JsonPage {
                if ((post == null) || (post.getSone() == null)) {
                        return createErrorJsonObject("invalid-post-id");
                }
+               text = TextFilter.filter(request.getHttpRequest().getHeader("host"), text);
                Reply reply = webInterface.getCore().createReply(sender, post, text);
                return createSuccessJsonObject().put("reply", reply.getId()).put("sone", sender.getId());
        }
index 8d3b414..37ab040 100644 (file)
@@ -19,6 +19,7 @@ package net.pterodactylus.sone.web.ajax;
 
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.json.JsonObject;
 
 /**
@@ -46,7 +47,7 @@ public class DeletePostAjaxPage extends JsonPage {
         * {@inheritDoc}
         */
        @Override
-       protected JsonObject createJsonObject(Request request) {
+       protected JsonObject createJsonObject(FreenetRequest request) {
                String postId = request.getHttpRequest().getParam("post");
                Post post = webInterface.getCore().getPost(postId, false);
                if ((post == null) || (post.getSone() == null)) {
index 383d2d4..205fb85 100644 (file)
@@ -21,6 +21,7 @@ import net.pterodactylus.sone.data.Profile;
 import net.pterodactylus.sone.data.Profile.Field;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.json.JsonObject;
 
 /**
@@ -44,7 +45,7 @@ public class DeleteProfileFieldAjaxPage extends JsonPage {
         * {@inheritDoc}
         */
        @Override
-       protected JsonObject createJsonObject(Request request) {
+       protected JsonObject createJsonObject(FreenetRequest request) {
                String fieldId = request.getHttpRequest().getParam("field");
                Sone currentSone = getCurrentSone(request.getToadletContext());
                Profile profile = currentSone.getProfile();
@@ -54,7 +55,7 @@ public class DeleteProfileFieldAjaxPage extends JsonPage {
                }
                profile.removeField(field);
                currentSone.setProfile(profile);
-               webInterface.getCore().saveSone(currentSone);
+               webInterface.getCore().touchConfiguration();
                return createSuccessJsonObject().put("field", new JsonObject().put("id", field.getId()));
        }
 
index f34d202..7bea4cf 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - DeleteReplysAjaxPage.java - Copyright © 2010 David Roden
+ * Sone - DeleteReplyAjaxPage.java - Copyright © 2010 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
@@ -19,6 +19,7 @@ package net.pterodactylus.sone.web.ajax;
 
 import net.pterodactylus.sone.data.Reply;
 import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.json.JsonObject;
 
 /**
@@ -46,7 +47,7 @@ public class DeleteReplyAjaxPage extends JsonPage {
         * {@inheritDoc}
         */
        @Override
-       protected JsonObject createJsonObject(Request request) {
+       protected JsonObject createJsonObject(FreenetRequest request) {
                String replyId = request.getHttpRequest().getParam("reply");
                Reply reply = webInterface.getCore().getReply(replyId);
                if (reply == null) {
index 08e3ee5..232cab6 100644 (file)
@@ -18,6 +18,7 @@
 package net.pterodactylus.sone.web.ajax;
 
 import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.json.JsonObject;
 import net.pterodactylus.util.notify.Notification;
 
@@ -42,7 +43,7 @@ public class DismissNotificationAjaxPage extends JsonPage {
         * {@inheritDoc}
         */
        @Override
-       protected JsonObject createJsonObject(Request request) {
+       protected JsonObject createJsonObject(FreenetRequest request) {
                String notificationId = request.getHttpRequest().getParam("notification");
                Notification notification = webInterface.getNotifications().getNotification(notificationId);
                if (notification == null) {
index 1a6d28f..4ba3dbb 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - TrustAjaxPage.java - Copyright © 2011 David Roden
+ * Sone - DistrustAjaxPage.java - Copyright © 2011 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
@@ -21,6 +21,7 @@ import net.pterodactylus.sone.core.Core;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.freenet.wot.Trust;
 import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.json.JsonObject;
 
 /**
@@ -45,7 +46,7 @@ public class DistrustAjaxPage extends JsonPage {
         * {@inheritDoc}
         */
        @Override
-       protected JsonObject createJsonObject(Request request) {
+       protected JsonObject createJsonObject(FreenetRequest request) {
                Sone currentSone = getCurrentSone(request.getToadletContext(), false);
                if (currentSone == null) {
                        return createErrorJsonObject("auth-required");
index 0036545..5351281 100644 (file)
@@ -21,6 +21,7 @@ import net.pterodactylus.sone.data.Profile;
 import net.pterodactylus.sone.data.Profile.Field;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.json.JsonObject;
 
 /**
@@ -48,7 +49,7 @@ public class EditProfileFieldAjaxPage extends JsonPage {
         * {@inheritDoc}
         */
        @Override
-       protected JsonObject createJsonObject(Request request) {
+       protected JsonObject createJsonObject(FreenetRequest request) {
                String fieldId = request.getHttpRequest().getParam("field");
                Sone currentSone = getCurrentSone(request.getToadletContext());
                Profile profile = currentSone.getProfile();
index 1269b41..5c3e5f4 100644 (file)
@@ -19,6 +19,7 @@ package net.pterodactylus.sone.web.ajax;
 
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.json.JsonObject;
 
 /**
@@ -42,7 +43,7 @@ public class FollowSoneAjaxPage extends JsonPage {
         * {@inheritDoc}
         */
        @Override
-       protected JsonObject createJsonObject(Request request) {
+       protected JsonObject createJsonObject(FreenetRequest request) {
                String soneId = request.getHttpRequest().getParam("sone");
                if (!webInterface.getCore().hasSone(soneId)) {
                        return createErrorJsonObject("invalid-sone-id");
@@ -52,7 +53,7 @@ public class FollowSoneAjaxPage extends JsonPage {
                        return createErrorJsonObject("auth-required");
                }
                currentSone.addFriend(soneId);
-               webInterface.getCore().saveSone(currentSone);
+               webInterface.getCore().touchConfiguration();
                return createSuccessJsonObject();
        }
 
index a8b991d..539be3d 100644 (file)
@@ -27,6 +27,7 @@ import net.pterodactylus.sone.data.Reply;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.template.SoneAccessor;
 import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.json.JsonArray;
 import net.pterodactylus.util.json.JsonObject;
 
@@ -55,7 +56,7 @@ public class GetLikesAjaxPage extends JsonPage {
         * {@inheritDoc}
         */
        @Override
-       protected JsonObject createJsonObject(Request request) {
+       protected JsonObject createJsonObject(FreenetRequest request) {
                String type = request.getHttpRequest().getParam("type", null);
                String id = request.getHttpRequest().getParam(type, null);
                if ((id == null) || (id.length() == 0)) {
diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/GetNotificationAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/GetNotificationAjaxPage.java
new file mode 100644 (file)
index 0000000..198a04f
--- /dev/null
@@ -0,0 +1,145 @@
+/*
+ * Sone - GetNotificationAjaxPage.java - Copyright © 2010 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.web.ajax;
+
+import java.io.IOException;
+import java.io.StringWriter;
+
+import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.sone.data.Reply;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.main.SonePlugin;
+import net.pterodactylus.sone.notify.ListNotification;
+import net.pterodactylus.sone.notify.ListNotificationFilters;
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
+import net.pterodactylus.util.json.JsonObject;
+import net.pterodactylus.util.notify.Notification;
+import net.pterodactylus.util.notify.TemplateNotification;
+import net.pterodactylus.util.template.TemplateContext;
+
+/**
+ * The “get notification” AJAX handler returns a number of rendered
+ * notifications.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class GetNotificationAjaxPage extends JsonPage {
+
+       /**
+        * Creates a new “get notification” AJAX page.
+        *
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public GetNotificationAjaxPage(WebInterface webInterface) {
+               super("getNotification.ajax", webInterface);
+       }
+
+       //
+       // JSONPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected boolean needsFormPassword() {
+               return false;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected boolean requiresLogin() {
+               return false;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       @SuppressWarnings("unchecked")
+       protected JsonObject createJsonObject(FreenetRequest request) {
+               String[] notificationIds = request.getHttpRequest().getParam("notifications").split(",");
+               JsonObject jsonNotifications = new JsonObject();
+               Sone currentSone = getCurrentSone(request.getToadletContext(), false);
+               for (String notificationId : notificationIds) {
+                       Notification notification = webInterface.getNotifications().getNotification(notificationId);
+                       if ("new-post-notification".equals(notificationId)) {
+                               notification = ListNotificationFilters.filterNewPostNotification((ListNotification<Post>) notification, currentSone, false);
+                       } else if ("new-reply-notification".equals(notificationId)) {
+                               notification = ListNotificationFilters.filterNewReplyNotification((ListNotification<Reply>) notification, currentSone);
+                       } else if ("mention-notification".equals(notificationId)) {
+                               notification = ListNotificationFilters.filterNewPostNotification((ListNotification<Post>) notification, currentSone, false);
+                       }
+                       if (notification == null) {
+                               // TODO - show error
+                               continue;
+                       }
+                       jsonNotifications.put(notificationId, createJsonNotification(request, notification));
+               }
+               return createSuccessJsonObject().put("notifications", jsonNotifications);
+       }
+
+       //
+       // PRIVATE METHODS
+       //
+
+       /**
+        * Creates a JSON object from the given notification.
+        *
+        * @param request
+        *            The request to load the session from
+        * @param notification
+        *            The notification to create a JSON object
+        * @return The JSON object
+        */
+       private JsonObject createJsonNotification(FreenetRequest request, Notification notification) {
+               JsonObject jsonNotification = new JsonObject();
+               jsonNotification.put("id", notification.getId());
+               StringWriter notificationWriter = new StringWriter();
+               try {
+                       if (notification instanceof TemplateNotification) {
+                               TemplateContext templateContext = webInterface.getTemplateContextFactory().createTemplateContext().mergeContext(((TemplateNotification) notification).getTemplateContext());
+                               templateContext.set("core", webInterface.getCore());
+                               templateContext.set("currentSone", webInterface.getCurrentSone(request.getToadletContext(), false));
+                               templateContext.set("localSones", webInterface.getCore().getLocalSones());
+                               templateContext.set("request", request);
+                               templateContext.set("currentVersion", SonePlugin.VERSION);
+                               templateContext.set("hasLatestVersion", webInterface.getCore().getUpdateChecker().hasLatestVersion());
+                               templateContext.set("latestEdition", webInterface.getCore().getUpdateChecker().getLatestEdition());
+                               templateContext.set("latestVersion", webInterface.getCore().getUpdateChecker().getLatestVersion());
+                               templateContext.set("latestVersionTime", webInterface.getCore().getUpdateChecker().getLatestVersionDate());
+                               templateContext.set("notification", notification);
+                               ((TemplateNotification) notification).render(templateContext, notificationWriter);
+                       } else {
+                               notification.render(notificationWriter);
+                       }
+               } catch (IOException ioe1) {
+                       /* StringWriter never throws, ignore. */
+               }
+               jsonNotification.put("text", notificationWriter.toString());
+               jsonNotification.put("createdTime", notification.getCreatedTime());
+               jsonNotification.put("lastUpdatedTime", notification.getLastUpdatedTime());
+               jsonNotification.put("dismissable", notification.isDismissable());
+               return jsonNotification;
+       }
+
+}
index 4e5f1b8..8be816d 100644 (file)
@@ -22,6 +22,7 @@ import java.io.StringWriter;
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.io.Closer;
 import net.pterodactylus.util.json.JsonObject;
 import net.pterodactylus.util.template.Template;
@@ -56,13 +57,13 @@ public class GetPostAjaxPage extends JsonPage {
         * {@inheritDoc}
         */
        @Override
-       protected JsonObject createJsonObject(Request request) {
+       protected JsonObject createJsonObject(FreenetRequest request) {
                String postId = request.getHttpRequest().getParam("post");
                Post post = webInterface.getCore().getPost(postId, false);
                if (post == null) {
                        return createErrorJsonObject("invalid-post-id");
                }
-               return createSuccessJsonObject().put("post", createJsonPost(post, getCurrentSone(request.getToadletContext())));
+               return createSuccessJsonObject().put("post", createJsonPost(request, post, getCurrentSone(request.getToadletContext())));
        }
 
        /**
@@ -81,13 +82,15 @@ public class GetPostAjaxPage extends JsonPage {
         * Creates a JSON object from the given post. The JSON object will only
         * contain the ID of the post, its time, and its rendered HTML code.
         *
+        * @param request
+        *            The request being processed
         * @param post
         *            The post to create a JSON object from
         * @param currentSone
         *            The currently logged in Sone (to store in the template)
         * @return The JSON representation of the post
         */
-       private JsonObject createJsonPost(Post post, Sone currentSone) {
+       private JsonObject createJsonPost(FreenetRequest request, Post post, Sone currentSone) {
                JsonObject jsonPost = new JsonObject();
                jsonPost.put("id", post.getId());
                jsonPost.put("sone", post.getSone().getId());
@@ -95,6 +98,8 @@ public class GetPostAjaxPage extends JsonPage {
                jsonPost.put("time", post.getTime());
                StringWriter stringWriter = new StringWriter();
                TemplateContext templateContext = webInterface.getTemplateContextFactory().createTemplateContext();
+               templateContext.set("core", webInterface.getCore());
+               templateContext.set("request", request);
                templateContext.set("post", post);
                templateContext.set("currentSone", currentSone);
                templateContext.set("localSones", webInterface.getCore().getLocalSones());
index c19bcba..2eef58a 100644 (file)
@@ -22,6 +22,7 @@ import java.io.StringWriter;
 import net.pterodactylus.sone.data.Reply;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.io.Closer;
 import net.pterodactylus.util.json.JsonObject;
 import net.pterodactylus.util.template.Template;
@@ -59,13 +60,13 @@ public class GetReplyAjaxPage extends JsonPage {
         * {@inheritDoc}
         */
        @Override
-       protected JsonObject createJsonObject(Request request) {
+       protected JsonObject createJsonObject(FreenetRequest request) {
                String replyId = request.getHttpRequest().getParam("reply");
                Reply reply = webInterface.getCore().getReply(replyId);
                if ((reply == null) || (reply.getSone() == null)) {
                        return createErrorJsonObject("invalid-reply-id");
                }
-               return createSuccessJsonObject().put("reply", createJsonReply(reply, getCurrentSone(request.getToadletContext())));
+               return createSuccessJsonObject().put("reply", createJsonReply(request, reply, getCurrentSone(request.getToadletContext())));
        }
 
        /**
@@ -83,13 +84,15 @@ public class GetReplyAjaxPage extends JsonPage {
        /**
         * Creates a JSON representation of the given reply.
         *
+        * @param request
+        *            The request being processed
         * @param reply
         *            The reply to convert
         * @param currentSone
         *            The currently logged in Sone (to store in the template)
         * @return The JSON representation of the reply
         */
-       private JsonObject createJsonReply(Reply reply, Sone currentSone) {
+       private JsonObject createJsonReply(FreenetRequest request, Reply reply, Sone currentSone) {
                JsonObject jsonReply = new JsonObject();
                jsonReply.put("id", reply.getId());
                jsonReply.put("postId", reply.getPost().getId());
@@ -97,6 +100,8 @@ public class GetReplyAjaxPage extends JsonPage {
                jsonReply.put("time", reply.getTime());
                StringWriter stringWriter = new StringWriter();
                TemplateContext templateContext = webInterface.getTemplateContextFactory().createTemplateContext();
+               templateContext.set("core", webInterface.getCore());
+               templateContext.set("request", request);
                templateContext.set("reply", reply);
                templateContext.set("currentSone", currentSone);
                try {
index 426c08f..eaa6506 100644 (file)
 
 package net.pterodactylus.sone.web.ajax;
 
-import java.io.IOException;
-import java.io.StringWriter;
 import java.text.DateFormat;
 import java.text.SimpleDateFormat;
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Date;
 import java.util.HashSet;
@@ -34,13 +31,12 @@ import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.notify.ListNotificationFilters;
 import net.pterodactylus.sone.template.SoneAccessor;
 import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.filter.Filter;
 import net.pterodactylus.util.filter.Filters;
 import net.pterodactylus.util.json.JsonArray;
 import net.pterodactylus.util.json.JsonObject;
 import net.pterodactylus.util.notify.Notification;
-import net.pterodactylus.util.notify.TemplateNotification;
-import net.pterodactylus.util.template.TemplateContext;
 
 /**
  * The “get status” AJAX handler returns all information that is necessary to
@@ -67,13 +63,22 @@ public class GetStatusAjaxPage extends JsonPage {
         * {@inheritDoc}
         */
        @Override
-       protected JsonObject createJsonObject(Request request) {
+       protected JsonObject createJsonObject(FreenetRequest request) {
                final Sone currentSone = getCurrentSone(request.getToadletContext(), false);
                /* load Sones. */
-               boolean loadAllSones = Boolean.parseBoolean(request.getHttpRequest().getParam("loadAllSones", "true"));
+               boolean loadAllSones = Boolean.parseBoolean(request.getHttpRequest().getParam("loadAllSones", "false"));
                Set<Sone> sones = new HashSet<Sone>(Collections.singleton(getCurrentSone(request.getToadletContext(), false)));
                if (loadAllSones) {
                        sones.addAll(webInterface.getCore().getSones());
+               } else {
+                       String loadSoneIds = request.getHttpRequest().getParam("soneIds");
+                       if (loadSoneIds.length() > 0) {
+                               String[] soneIds = loadSoneIds.split(",");
+                               for (String soneId : soneIds) {
+                                       /* just add it, we skip null further down. */
+                                       sones.add(webInterface.getCore().getSone(soneId, false));
+                               }
+                       }
                }
                JsonArray jsonSones = new JsonArray();
                for (Sone sone : sones) {
@@ -84,11 +89,11 @@ public class GetStatusAjaxPage extends JsonPage {
                        jsonSones.add(jsonSone);
                }
                /* load notifications. */
-               List<Notification> notifications = ListNotificationFilters.filterNotifications(new ArrayList<Notification>(webInterface.getNotifications().getNotifications()), currentSone);
+               List<Notification> notifications = ListNotificationFilters.filterNotifications(webInterface.getNotifications().getNotifications(), currentSone);
                Collections.sort(notifications, Notification.LAST_UPDATED_TIME_SORTER);
-               JsonArray jsonNotifications = new JsonArray();
+               JsonArray jsonNotificationInformations = new JsonArray();
                for (Notification notification : notifications) {
-                       jsonNotifications.add(createJsonNotification(notification));
+                       jsonNotificationInformations.add(createJsonNotificationInformation(notification));
                }
                /* load new posts. */
                Set<Post> newPosts = webInterface.getNewPosts();
@@ -97,7 +102,7 @@ public class GetStatusAjaxPage extends JsonPage {
 
                                @Override
                                public boolean filterObject(Post post) {
-                                       return currentSone.hasFriend(post.getSone().getId()) || currentSone.equals(post.getSone()) || currentSone.equals(post.getRecipient());
+                                       return ListNotificationFilters.isPostVisible(currentSone, post);
                                }
 
                        });
@@ -118,11 +123,19 @@ public class GetStatusAjaxPage extends JsonPage {
 
                                @Override
                                public boolean filterObject(Reply reply) {
-                                       return currentSone.hasFriend(reply.getPost().getSone().getId()) || currentSone.equals(reply.getPost().getSone()) || currentSone.equals(reply.getPost().getRecipient());
+                                       return ListNotificationFilters.isReplyVisible(currentSone, reply);
                                }
 
                        });
                }
+               /* remove replies to unknown posts. */
+               newReplies = Filters.filteredSet(newReplies, new Filter<Reply>() {
+
+                       @Override
+                       public boolean filterObject(Reply reply) {
+                               return (reply.getPost() != null) && (reply.getPost().getSone() != null);
+                       }
+               });
                JsonArray jsonReplies = new JsonArray();
                for (Reply reply : newReplies) {
                        JsonObject jsonReply = new JsonObject();
@@ -132,7 +145,7 @@ public class GetStatusAjaxPage extends JsonPage {
                        jsonReply.put("postSone", reply.getPost().getSone().getId());
                        jsonReplies.add(jsonReply);
                }
-               return createSuccessJsonObject().put("sones", jsonSones).put("notifications", jsonNotifications).put("newPosts", jsonPosts).put("newReplies", jsonReplies);
+               return createSuccessJsonObject().put("loggedIn", currentSone != null).put("sones", jsonSones).put("notifications", jsonNotificationInformations).put("newPosts", jsonPosts).put("newReplies", jsonReplies);
        }
 
        /**
@@ -174,36 +187,25 @@ public class GetStatusAjaxPage extends JsonPage {
                synchronized (dateFormat) {
                        jsonSone.put("lastUpdated", dateFormat.format(new Date(sone.getTime())));
                }
-               jsonSone.put("age", (System.currentTimeMillis() - sone.getTime()) / 1000);
+               jsonSone.put("lastUpdatedText", GetTimesAjaxPage.getTime(webInterface, sone.getTime()).getText());
                return jsonSone;
        }
 
        /**
-        * Creates a JSON object from the given notification.
+        * Creates a JSON object that only contains the ID and the last-updated time
+        * of the given notification.
         *
+        * @see Notification#getId()
+        * @see Notification#getLastUpdatedTime()
         * @param notification
-        *            The notification to create a JSON object
-        * @return The JSON object
+        *            The notification
+        * @return A JSON object containing the notification ID and last-updated
+        *         time
         */
-       private JsonObject createJsonNotification(Notification notification) {
+       private JsonObject createJsonNotificationInformation(Notification notification) {
                JsonObject jsonNotification = new JsonObject();
                jsonNotification.put("id", notification.getId());
-               StringWriter notificationWriter = new StringWriter();
-               try {
-                       if (notification instanceof TemplateNotification) {
-                               TemplateContext templateContext = webInterface.getTemplateContextFactory().createTemplateContext().mergeContext(((TemplateNotification) notification).getTemplateContext());
-                               templateContext.set("notification", notification);
-                               ((TemplateNotification) notification).render(templateContext, notificationWriter);
-                       } else {
-                               notification.render(notificationWriter);
-                       }
-               } catch (IOException ioe1) {
-                       /* StringWriter never throws, ignore. */
-               }
-               jsonNotification.put("text", notificationWriter.toString());
-               jsonNotification.put("createdTime", notification.getCreatedTime());
                jsonNotification.put("lastUpdatedTime", notification.getLastUpdatedTime());
-               jsonNotification.put("dismissable", notification.isDismissable());
                return jsonNotification;
        }
 
index b17e4a2..1711a5b 100644 (file)
@@ -24,6 +24,7 @@ import java.util.Date;
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Reply;
 import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.json.JsonObject;
 import net.pterodactylus.util.number.Digits;
 
@@ -51,8 +52,7 @@ public class GetTimesAjaxPage extends JsonPage {
         * {@inheritDoc}
         */
        @Override
-       protected JsonObject createJsonObject(Request request) {
-               long now = System.currentTimeMillis();
+       protected JsonObject createJsonObject(FreenetRequest request) {
                String allIds = request.getHttpRequest().getParam("posts");
                JsonObject postTimes = new JsonObject();
                if (allIds.length() > 0) {
@@ -62,9 +62,8 @@ public class GetTimesAjaxPage extends JsonPage {
                                if (post == null) {
                                        continue;
                                }
-                               long age = now - post.getTime();
                                JsonObject postTime = new JsonObject();
-                               Time time = getTime(age);
+                               Time time = getTime(post.getTime());
                                postTime.put("timeText", time.getText());
                                postTime.put("refreshTime", time.getRefresh() / Time.SECOND);
                                postTime.put("tooltip", dateFormat.format(new Date(post.getTime())));
@@ -80,9 +79,8 @@ public class GetTimesAjaxPage extends JsonPage {
                                if (reply == null) {
                                        continue;
                                }
-                               long age = now - reply.getTime();
                                JsonObject replyTime = new JsonObject();
-                               Time time = getTime(age);
+                               Time time = getTime(reply.getTime());
                                replyTime.put("timeText", time.getText());
                                replyTime.put("refreshTime", time.getRefresh() / Time.SECOND);
                                replyTime.put("tooltip", dateFormat.format(new Date(reply.getTime())));
@@ -113,13 +111,34 @@ public class GetTimesAjaxPage extends JsonPage {
        //
 
        /**
-        * Returns the formatted relative time for a given age.
+        * Returns the formatted relative time for a given time.
         *
-        * @param age
-        *            The age to format (in milliseconds)
+        * @param time
+        *            The time to format the difference from (in milliseconds)
         * @return The formatted age
         */
-       private Time getTime(long age) {
+       private Time getTime(long time) {
+               return getTime(webInterface, time);
+       }
+
+       //
+       // STATIC METHODS
+       //
+
+       /**
+        * Returns the formatted relative time for a given time.
+        *
+        * @param webInterface
+        *            The Sone web interface (for l10n access)
+        * @param time
+        *            The time to format the difference from (in milliseconds)
+        * @return The formatted age
+        */
+       public static Time getTime(WebInterface webInterface, long time) {
+               if (time == 0) {
+                       return new Time(webInterface.getL10n().getString("View.Sone.Text.UnknownDate"), 12 * Time.HOUR);
+               }
+               long age = System.currentTimeMillis() - time;
                String text;
                long refresh;
                if (age < 0) {
@@ -135,7 +154,7 @@ public class GetTimesAjaxPage extends JsonPage {
                        text = webInterface.getL10n().getString("View.Time.AMinuteAgo");
                        refresh = Time.MINUTE;
                } else if (age < 30 * Time.MINUTE) {
-                       text = webInterface.getL10n().getString("View.Time.XMinutesAgo", "min", String.valueOf((int) Digits.round(age / Time.MINUTE, 1)));
+                       text = webInterface.getL10n().getString("View.Time.XMinutesAgo", "min", String.valueOf((int) (Digits.round(age, Time.MINUTE) / Time.MINUTE)));
                        refresh = 1 * Time.MINUTE;
                } else if (age < 45 * Time.MINUTE) {
                        text = webInterface.getL10n().getString("View.Time.HalfAnHourAgo");
@@ -144,31 +163,31 @@ public class GetTimesAjaxPage extends JsonPage {
                        text = webInterface.getL10n().getString("View.Time.AnHourAgo");
                        refresh = Time.HOUR;
                } else if (age < 21 * Time.HOUR) {
-                       text = webInterface.getL10n().getString("View.Time.XHoursAgo", "hour", String.valueOf((int) Digits.round(age / Time.HOUR, 1)));
+                       text = webInterface.getL10n().getString("View.Time.XHoursAgo", "hour", String.valueOf((int) (Digits.round(age, Time.HOUR) / Time.HOUR)));
                        refresh = Time.HOUR;
                } else if (age < 42 * Time.HOUR) {
                        text = webInterface.getL10n().getString("View.Time.ADayAgo");
                        refresh = Time.DAY;
                } else if (age < 6 * Time.DAY) {
-                       text = webInterface.getL10n().getString("View.Time.XDaysAgo", "day", String.valueOf((int) Digits.round(age / Time.DAY, 1)));
+                       text = webInterface.getL10n().getString("View.Time.XDaysAgo", "day", String.valueOf((int) (Digits.round(age, Time.DAY) / Time.DAY)));
                        refresh = Time.DAY;
                } else if (age < 11 * Time.DAY) {
                        text = webInterface.getL10n().getString("View.Time.AWeekAgo");
                        refresh = Time.DAY;
                } else if (age < 4 * Time.WEEK) {
-                       text = webInterface.getL10n().getString("View.Time.XWeeksAgo", "week", String.valueOf((int) Digits.round(age / Time.WEEK, 1)));
+                       text = webInterface.getL10n().getString("View.Time.XWeeksAgo", "week", String.valueOf((int) (Digits.round(age, Time.WEEK) / Time.WEEK)));
                        refresh = Time.DAY;
                } else if (age < 6 * Time.WEEK) {
                        text = webInterface.getL10n().getString("View.Time.AMonthAgo");
                        refresh = Time.DAY;
                } else if (age < 11 * Time.MONTH) {
-                       text = webInterface.getL10n().getString("View.Time.XMonthsAgo", "month", String.valueOf((int) Digits.round(age / Time.MONTH, 1)));
+                       text = webInterface.getL10n().getString("View.Time.XMonthsAgo", "month", String.valueOf((int) (Digits.round(age, Time.MONTH) / Time.MONTH)));
                        refresh = Time.DAY;
                } else if (age < 18 * Time.MONTH) {
                        text = webInterface.getL10n().getString("View.Time.AYearAgo");
                        refresh = Time.WEEK;
                } else {
-                       text = webInterface.getL10n().getString("View.Time.XYearsAgo", "year", String.valueOf((int) Digits.round(age / Time.YEAR, 1)));
+                       text = webInterface.getL10n().getString("View.Time.XYearsAgo", "year", String.valueOf((int) (Digits.round(age, Time.YEAR) / Time.YEAR)));
                        refresh = Time.WEEK;
                }
                return new Time(text, refresh);
@@ -179,7 +198,7 @@ public class GetTimesAjaxPage extends JsonPage {
         *
         * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
         */
-       private static class Time {
+       public static class Time {
 
                /** Number of milliseconds in a second. */
                private static final long SECOND = 1000;
@@ -239,6 +258,14 @@ public class GetTimesAjaxPage extends JsonPage {
                        return refresh;
                }
 
+               /**
+                * {@inheritDoc}
+                */
+               @Override
+               public String toString() {
+                       return text;
+               }
+
        }
 
 }
index 725b13a..e0909ca 100644 (file)
@@ -18,6 +18,7 @@
 package net.pterodactylus.sone.web.ajax;
 
 import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.json.JsonObject;
 
 /**
@@ -45,7 +46,7 @@ public class GetTranslationPage extends JsonPage {
         * {@inheritDoc}
         */
        @Override
-       protected JsonObject createJsonObject(Request request) {
+       protected JsonObject createJsonObject(FreenetRequest request) {
                String key = request.getHttpRequest().getParam("key");
                String translation = webInterface.getL10n().getString(key);
                return createSuccessJsonObject().put("value", translation);
index 8d48bce..1e5e8ed 100644 (file)
 
 package net.pterodactylus.sone.web.ajax;
 
+import java.io.IOException;
+
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.web.WebInterface;
-import net.pterodactylus.sone.web.page.Page;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.json.JsonObject;
 import net.pterodactylus.util.json.JsonUtils;
+import net.pterodactylus.util.web.Page;
+import net.pterodactylus.util.web.Response;
 import freenet.clients.http.SessionManager.Session;
 import freenet.clients.http.ToadletContext;
 
@@ -31,7 +35,7 @@ import freenet.clients.http.ToadletContext;
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
-public abstract class JsonPage implements Page {
+public abstract class JsonPage implements Page<FreenetRequest> {
 
        /** The path of the page. */
        private final String path;
@@ -124,7 +128,7 @@ public abstract class JsonPage implements Page {
         *            The request to handle
         * @return The created JSON object
         */
-       protected abstract JsonObject createJsonObject(Request request);
+       protected abstract JsonObject createJsonObject(FreenetRequest request);
 
        /**
         * Returns whether this command needs the form password for authentication
@@ -187,20 +191,31 @@ public abstract class JsonPage implements Page {
         * {@inheritDoc}
         */
        @Override
-       public Response handleRequest(Request request) {
+       public boolean isPrefixPage() {
+               return false;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public Response handleRequest(FreenetRequest request, Response response) throws IOException {
+               if (webInterface.getCore().getPreferences().isRequireFullAccess() && !request.getToadletContext().isAllowedFullAccess()) {
+                       return response.setStatusCode(403).setStatusText("Forbidden").setContentType("application/json").write(JsonUtils.format(new JsonObject().put("success", false).put("error", "auth-required")));
+               }
                if (needsFormPassword()) {
                        String formPassword = request.getHttpRequest().getParam("formPassword");
                        if (!webInterface.getFormPassword().equals(formPassword)) {
-                               return new Response(401, "Not authorized", "application/json", JsonUtils.format(new JsonObject().put("success", false).put("error", "auth-required")));
+                               return response.setStatusCode(403).setStatusText("Forbidden").setContentType("application/json").write(JsonUtils.format(new JsonObject().put("success", false).put("error", "auth-required")));
                        }
                }
                if (requiresLogin()) {
                        if (getCurrentSone(request.getToadletContext(), false) == null) {
-                               return new Response(401, "Not authorized", "application/json", JsonUtils.format(createErrorJsonObject("auth-required")));
+                               return response.setStatusCode(403).setStatusText("Forbidden").setContentType("application/json").write(JsonUtils.format(new JsonObject().put("success", false).put("error", "auth-required")));
                        }
                }
                JsonObject jsonObject = createJsonObject(request);
-               return new Response(200, "OK", "application/json", JsonUtils.format(jsonObject));
+               return response.setStatusCode(200).setStatusText("OK").setContentType("application/json").write(JsonUtils.format(jsonObject));
        }
 
 }
index efd1439..e4d168f 100644 (file)
@@ -20,6 +20,7 @@ package net.pterodactylus.sone.web.ajax;
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.json.JsonObject;
 
 /**
@@ -43,7 +44,7 @@ public class LikeAjaxPage extends JsonPage {
         * {@inheritDoc}
         */
        @Override
-       protected JsonObject createJsonObject(Request request) {
+       protected JsonObject createJsonObject(FreenetRequest request) {
                String type = request.getHttpRequest().getParam("type", null);
                String id = request.getHttpRequest().getParam(type, null);
                if ((id == null) || (id.length() == 0)) {
@@ -55,10 +56,10 @@ public class LikeAjaxPage extends JsonPage {
                }
                if ("post".equals(type)) {
                        currentSone.addLikedPostId(id);
-                       webInterface.getCore().saveSone(currentSone);
+                       webInterface.getCore().touchConfiguration();
                } else if ("reply".equals(type)) {
                        currentSone.addLikedReplyId(id);
-                       webInterface.getCore().saveSone(currentSone);
+                       webInterface.getCore().touchConfiguration();
                } else {
                        return createErrorJsonObject("invalid-type");
                }
index bca9a35..1d80905 100644 (file)
@@ -20,6 +20,7 @@ package net.pterodactylus.sone.web.ajax;
 import net.pterodactylus.sone.core.Core;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.json.JsonObject;
 
 /**
@@ -43,7 +44,7 @@ public class LockSoneAjaxPage extends JsonPage {
         * {@inheritDoc}
         */
        @Override
-       protected JsonObject createJsonObject(Request request) {
+       protected JsonObject createJsonObject(FreenetRequest request) {
                String soneId = request.getHttpRequest().getParam("sone");
                Sone sone = webInterface.getCore().getLocalSone(soneId, false);
                if (sone == null) {
index b641ea0..42ee8b2 100644 (file)
@@ -22,6 +22,7 @@ import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Reply;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.json.JsonObject;
 
 /**
@@ -46,7 +47,7 @@ public class MarkAsKnownAjaxPage extends JsonPage {
         * {@inheritDoc}
         */
        @Override
-       protected JsonObject createJsonObject(Request request) {
+       protected JsonObject createJsonObject(FreenetRequest request) {
                String type = request.getHttpRequest().getParam("type");
                if (!type.equals("sone") && !type.equals("post") && !type.equals("reply")) {
                        return createErrorJsonObject("invalid-type");
index 780e4d1..b932a82 100644 (file)
@@ -21,6 +21,7 @@ import net.pterodactylus.sone.data.Profile;
 import net.pterodactylus.sone.data.Profile.Field;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.json.JsonObject;
 
 /**
@@ -50,7 +51,7 @@ public class MoveProfileFieldAjaxPage extends JsonPage {
         * {@inheritDoc}
         */
        @Override
-       protected JsonObject createJsonObject(Request request) {
+       protected JsonObject createJsonObject(FreenetRequest request) {
                Sone currentSone = getCurrentSone(request.getToadletContext());
                Profile profile = currentSone.getProfile();
                String fieldId = request.getHttpRequest().getParam("field");
@@ -71,7 +72,7 @@ public class MoveProfileFieldAjaxPage extends JsonPage {
                        return createErrorJsonObject("not-possible");
                }
                currentSone.setProfile(profile);
-               webInterface.getCore().saveSone(currentSone);
+               webInterface.getCore().touchConfiguration();
                return createSuccessJsonObject();
        }
 
index d6e2750..b27bedf 100644 (file)
@@ -21,6 +21,7 @@ import net.pterodactylus.sone.core.Core;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.freenet.wot.Trust;
 import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.json.JsonObject;
 
 /**
@@ -45,7 +46,7 @@ public class TrustAjaxPage extends JsonPage {
         * {@inheritDoc}
         */
        @Override
-       protected JsonObject createJsonObject(Request request) {
+       protected JsonObject createJsonObject(FreenetRequest request) {
                Sone currentSone = getCurrentSone(request.getToadletContext(), false);
                if (currentSone == null) {
                        return createErrorJsonObject("auth-required");
index ad1689e..6a9f8fe 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - BookmarkAjaxPage.java - Copyright © 2011 David Roden
+ * Sone - UnbookmarkAjaxPage.java - Copyright © 2011 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
@@ -18,6 +18,7 @@
 package net.pterodactylus.sone.web.ajax;
 
 import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.json.JsonObject;
 
 /**
@@ -45,7 +46,7 @@ public class UnbookmarkAjaxPage extends JsonPage {
         * {@inheritDoc}
         */
        @Override
-       protected JsonObject createJsonObject(Request request) {
+       protected JsonObject createJsonObject(FreenetRequest request) {
                String id = request.getHttpRequest().getParam("post", null);
                if ((id == null) || (id.length() == 0)) {
                        return createErrorJsonObject("invalid-post-id");
index c2379e8..26f1a11 100644 (file)
@@ -19,6 +19,7 @@ package net.pterodactylus.sone.web.ajax;
 
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.json.JsonObject;
 
 /**
@@ -42,7 +43,7 @@ public class UnfollowSoneAjaxPage extends JsonPage {
         * {@inheritDoc}
         */
        @Override
-       protected JsonObject createJsonObject(Request request) {
+       protected JsonObject createJsonObject(FreenetRequest request) {
                String soneId = request.getHttpRequest().getParam("sone");
                if (!webInterface.getCore().hasSone(soneId)) {
                        return createErrorJsonObject("invalid-sone-id");
@@ -52,7 +53,7 @@ public class UnfollowSoneAjaxPage extends JsonPage {
                        return createErrorJsonObject("auth-required");
                }
                currentSone.removeFriend(soneId);
-               webInterface.getCore().saveSone(currentSone);
+               webInterface.getCore().touchConfiguration();
                return createSuccessJsonObject();
        }
 
index e5c933d..1841806 100644 (file)
@@ -20,6 +20,7 @@ package net.pterodactylus.sone.web.ajax;
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.json.JsonObject;
 
 /**
@@ -43,7 +44,7 @@ public class UnlikeAjaxPage extends JsonPage {
         * {@inheritDoc}
         */
        @Override
-       protected JsonObject createJsonObject(Request request) {
+       protected JsonObject createJsonObject(FreenetRequest request) {
                String type = request.getHttpRequest().getParam("type", null);
                String id = request.getHttpRequest().getParam(type, null);
                if ((id == null) || (id.length() == 0)) {
@@ -55,10 +56,10 @@ public class UnlikeAjaxPage extends JsonPage {
                }
                if ("post".equals(type)) {
                        currentSone.removeLikedPostId(id);
-                       webInterface.getCore().saveSone(currentSone);
+                       webInterface.getCore().touchConfiguration();
                } else if ("reply".equals(type)) {
                        currentSone.removeLikedReplyId(id);
-                       webInterface.getCore().saveSone(currentSone);
+                       webInterface.getCore().touchConfiguration();
                } else {
                        return createErrorJsonObject("invalid-type");
                }
index e1c408c..d7430c1 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - LockSoneAjaxPage.java - Copyright © 2010 David Roden
+ * Sone - UnlockSoneAjaxPage.java - Copyright © 2010 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
@@ -20,6 +20,7 @@ package net.pterodactylus.sone.web.ajax;
 import net.pterodactylus.sone.core.Core;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.json.JsonObject;
 
 /**
@@ -43,7 +44,7 @@ public class UnlockSoneAjaxPage extends JsonPage {
         * {@inheritDoc}
         */
        @Override
-       protected JsonObject createJsonObject(Request request) {
+       protected JsonObject createJsonObject(FreenetRequest request) {
                String soneId = request.getHttpRequest().getParam("sone");
                Sone sone = webInterface.getCore().getLocalSone(soneId, false);
                if (sone == null) {
index 32c6229..86222e0 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Sone - TrustAjaxPage.java - Copyright © 2011 David Roden
+ * Sone - UntrustAjaxPage.java - Copyright © 2011 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
@@ -21,6 +21,7 @@ import net.pterodactylus.sone.core.Core;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.freenet.wot.Trust;
 import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.json.JsonObject;
 
 /**
@@ -45,7 +46,7 @@ public class UntrustAjaxPage extends JsonPage {
         * {@inheritDoc}
         */
        @Override
-       protected JsonObject createJsonObject(Request request) {
+       protected JsonObject createJsonObject(FreenetRequest request) {
                Sone currentSone = getCurrentSone(request.getToadletContext(), false);
                if (currentSone == null) {
                        return createErrorJsonObject("auth-required");
diff --git a/src/main/java/net/pterodactylus/sone/web/page/FreenetRequest.java b/src/main/java/net/pterodactylus/sone/web/page/FreenetRequest.java
new file mode 100644 (file)
index 0000000..526f3ed
--- /dev/null
@@ -0,0 +1,80 @@
+/*
+ * Sone - FreenetRequest.java - Copyright © 2011 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.web.page;
+
+import java.net.URI;
+
+import net.pterodactylus.util.web.Method;
+import net.pterodactylus.util.web.Request;
+import freenet.clients.http.ToadletContext;
+import freenet.support.api.HTTPRequest;
+
+/**
+ * Encapsulates all Freenet-specific properties of a request.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class FreenetRequest extends Request {
+
+       /** The underlying HTTP request from Freenet. */
+       private final HTTPRequest httpRequest;
+
+       /** The toadlet context. */
+       private final ToadletContext toadletContext;
+
+       /**
+        * Creates a new freenet request.
+        *
+        * @param uri
+        *            The URI that is being accessed
+        * @param method
+        *            The method used to access this page
+        * @param httpRequest
+        *            The underlying HTTP request from Freenet
+        * @param toadletContext
+        *            The toadlet context
+        */
+       public FreenetRequest(URI uri, Method method, HTTPRequest httpRequest, ToadletContext toadletContext) {
+               super(uri, method);
+               this.httpRequest = httpRequest;
+               this.toadletContext = toadletContext;
+       }
+
+       //
+       // ACCESSORS
+       //
+
+       /**
+        * Returns the underlying HTTP request from Freenet.
+        *
+        * @return The underlying HTTP request from Freenet
+        */
+       public HTTPRequest getHttpRequest() {
+               return httpRequest;
+       }
+
+       /**
+        * Returns the toadlet context.
+        *
+        * @return The toadlet context
+        */
+       public ToadletContext getToadletContext() {
+               return toadletContext;
+       }
+
+}
index 0668154..8096a6a 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * shortener - TemplatePage.java - Copyright © 2010 David Roden
+ * Sone - FreenetTemplatePage.java - Copyright © 2010 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
@@ -17,6 +17,7 @@
 
 package net.pterodactylus.sone.web.page;
 
+import java.io.IOException;
 import java.io.StringWriter;
 import java.util.Collection;
 import java.util.Collections;
@@ -26,11 +27,14 @@ import java.util.Map.Entry;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
-import net.pterodactylus.sone.web.page.Page.Request.Method;
 import net.pterodactylus.util.logging.Logging;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
 import net.pterodactylus.util.template.TemplateContextFactory;
+import net.pterodactylus.util.web.Method;
+import net.pterodactylus.util.web.Page;
+import net.pterodactylus.util.web.RedirectResponse;
+import net.pterodactylus.util.web.Response;
 import freenet.clients.http.LinkEnabledCallback;
 import freenet.clients.http.PageMaker;
 import freenet.clients.http.PageNode;
@@ -43,7 +47,7 @@ import freenet.support.HTMLNode;
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
-public class FreenetTemplatePage implements Page, LinkEnabledCallback {
+public class FreenetTemplatePage implements Page<FreenetRequest>, LinkEnabledCallback {
 
        /** The logger. */
        private static final Logger logger = Logging.getLogger(FreenetTemplatePage.class);
@@ -95,7 +99,7 @@ public class FreenetTemplatePage implements Page, LinkEnabledCallback {
         *            The request to serve
         * @return The title of the page
         */
-       protected String getPageTitle(Request request) {
+       protected String getPageTitle(FreenetRequest request) {
                return null;
        }
 
@@ -103,12 +107,23 @@ public class FreenetTemplatePage implements Page, LinkEnabledCallback {
         * {@inheritDoc}
         */
        @Override
-       public Response handleRequest(Request request) {
+       public boolean isPrefixPage() {
+               return false;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public Response handleRequest(FreenetRequest request, Response response) throws IOException {
                String redirectTarget = getRedirectTarget(request);
                if (redirectTarget != null) {
                        return new RedirectResponse(redirectTarget);
                }
 
+               if (isFullAccessOnly() && !request.getToadletContext().isAllowedFullAccess()) {
+                       return response.setStatusCode(401).setStatusText("Not authorized").setContentType("text/html");
+               }
                ToadletContext toadletContext = request.getToadletContext();
                if (request.getMethod() == Method.POST) {
                        /* require form password. */
@@ -150,7 +165,7 @@ public class FreenetTemplatePage implements Page, LinkEnabledCallback {
 
                postProcess(request, templateContext);
 
-               return new Response(200, "OK", "text/html", pageNode.outer.generate());
+               return response.setStatusCode(200).setStatusText("OK").setContentType("text/html").write(pageNode.outer.generate());
        }
 
        /**
@@ -183,7 +198,7 @@ public class FreenetTemplatePage implements Page, LinkEnabledCallback {
         * @throws RedirectException
         *             if the processing page wants to redirect after processing
         */
-       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                /* do nothing. */
        }
 
@@ -200,7 +215,7 @@ public class FreenetTemplatePage implements Page, LinkEnabledCallback {
         * @param templateContext
         *            The template context that supplied the rendered data
         */
-       protected void postProcess(Request request, TemplateContext templateContext) {
+       protected void postProcess(FreenetRequest request, TemplateContext templateContext) {
                /* do nothing. */
        }
 
@@ -212,7 +227,7 @@ public class FreenetTemplatePage implements Page, LinkEnabledCallback {
         *            The request that is processed
         * @return The URL to redirect to, or {@code null} to not redirect
         */
-       protected String getRedirectTarget(Page.Request request) {
+       protected String getRedirectTarget(FreenetRequest request) {
                return null;
        }
 
@@ -223,10 +238,21 @@ public class FreenetTemplatePage implements Page, LinkEnabledCallback {
         *            The request for which to return the link nodes
         * @return All link nodes that should be added to the HTML head
         */
-       protected List<Map<String, String>> getAdditionalLinkNodes(Request request) {
+       protected List<Map<String, String>> getAdditionalLinkNodes(FreenetRequest request) {
                return Collections.emptyList();
        }
 
+       /**
+        * Returns whether this page should only be allowed for requests from hosts
+        * with full access.
+        *
+        * @return {@code true} if this page should only be allowed for hosts with
+        *         full access, {@code false} to allow this page for any host
+        */
+       protected boolean isFullAccessOnly() {
+               return false;
+       }
+
        //
        // INTERFACE LinkEnabledCallback
        //
@@ -236,7 +262,7 @@ public class FreenetTemplatePage implements Page, LinkEnabledCallback {
         */
        @Override
        public boolean isEnabled(ToadletContext toadletContext) {
-               return true;
+               return !isFullAccessOnly();
        }
 
        /**
diff --git a/src/main/java/net/pterodactylus/sone/web/page/Page.java b/src/main/java/net/pterodactylus/sone/web/page/Page.java
deleted file mode 100644 (file)
index d26da79..0000000
+++ /dev/null
@@ -1,404 +0,0 @@
-/*
- * shortener - Page.java - Copyright © 2010 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.web.page;
-
-import java.io.ByteArrayInputStream;
-import java.io.InputStream;
-import java.io.UnsupportedEncodingException;
-import java.net.URI;
-import java.util.HashMap;
-import java.util.Map;
-
-import freenet.clients.http.ToadletContext;
-import freenet.support.api.HTTPRequest;
-
-/**
- * A page is responsible for handling HTTP requests and creating appropriate
- * responses.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public interface Page {
-
-       /**
-        * Returns the path of this toadlet.
-        *
-        * @return The path of this toadlet
-        */
-       public String getPath();
-
-       /**
-        * Handles a request.
-        *
-        * @param request
-        *            The request to handle
-        * @return The response
-        */
-       public Response handleRequest(Request request);
-
-       /**
-        * Container for request data.
-        *
-        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
-        */
-       public class Request {
-
-               /**
-                * Enumeration for all possible HTTP request methods.
-                *
-                * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’
-                *         Roden</a>
-                */
-               public enum Method {
-
-                       /** GET. */
-                       GET,
-
-                       /** POST. */
-                       POST,
-
-                       /** PUT. */
-                       PUT,
-
-                       /** DELETE. */
-                       DELETE,
-
-                       /** HEAD. */
-                       HEAD,
-
-                       /** OPTIONS. */
-                       OPTIONS,
-
-                       /** TRACE. */
-                       TRACE,
-
-               }
-
-               /** The URI that was accessed. */
-               private final URI uri;
-
-               /** The HTTP method that was used. */
-               private final Method method;
-
-               /** The HTTP request. */
-               private final HTTPRequest httpRequest;
-
-               /** The toadlet context. */
-               private final ToadletContext toadletContext;
-
-               /**
-                * Creates a new request that holds the given data.
-                *
-                * @param uri
-                *            The URI of the request
-                * @param method
-                *            The HTTP method of the request
-                * @param httpRequest
-                *            The HTTP request
-                * @param toadletContext
-                *            The toadlet context of the request
-                */
-               public Request(URI uri, Method method, HTTPRequest httpRequest, ToadletContext toadletContext) {
-                       this.uri = uri;
-                       this.method = method;
-                       this.httpRequest = httpRequest;
-                       this.toadletContext = toadletContext;
-               }
-
-               /**
-                * Returns the URI that was accessed.
-                *
-                * @return The accessed URI
-                */
-               public URI getUri() {
-                       return uri;
-               }
-
-               /**
-                * Returns the HTTP method that was used to access the page.
-                *
-                * @return The HTTP method
-                */
-               public Method getMethod() {
-                       return method;
-               }
-
-               /**
-                * Returns the HTTP request.
-                *
-                * @return The HTTP request
-                */
-               public HTTPRequest getHttpRequest() {
-                       return httpRequest;
-               }
-
-               /**
-                * Returns the toadlet context.
-                *
-                * @return The toadlet context
-                */
-               public ToadletContext getToadletContext() {
-                       return toadletContext;
-               }
-
-       }
-
-       /**
-        * Container for the HTTP response of a {@link Page}.
-        *
-        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
-        */
-       public class Response {
-
-               /** The HTTP status code of the response. */
-               private final int statusCode;
-
-               /** The HTTP status text of the response. */
-               private final String statusText;
-
-               /** The content type of the response. */
-               private final String contentType;
-
-               /** The headers of the response. */
-               private final Map<String, String> headers;
-
-               /** The content of the response body. */
-               private final InputStream content;
-
-               /**
-                * Creates a new response.
-                *
-                * @param statusCode
-                *            The HTTP status code of the response
-                * @param statusText
-                *            The HTTP status text of the response
-                * @param contentType
-                *            The content type of the response
-                * @param text
-                *            The text in the response body
-                */
-               public Response(int statusCode, String statusText, String contentType, String text) {
-                       this(statusCode, statusText, contentType, getBytes(text));
-               }
-
-               /**
-                * Creates a new response.
-                *
-                * @param statusCode
-                *            The HTTP status code of the response
-                * @param statusText
-                *            The HTTP status text of the response
-                * @param contentType
-                *            The content type of the response
-                * @param content
-                *            The content of the reponse body
-                */
-               public Response(int statusCode, String statusText, String contentType, byte[] content) {
-                       this(statusCode, statusText, contentType, new HashMap<String, String>(), content);
-               }
-
-               /**
-                * Creates a new response.
-                *
-                * @param statusCode
-                *            The HTTP status code of the response
-                * @param statusText
-                *            The HTTP status text of the response
-                * @param contentType
-                *            The content type of the response
-                * @param headers
-                *            The headers of the response
-                */
-               public Response(int statusCode, String statusText, String contentType, Map<String, String> headers) {
-                       this(statusCode, statusText, contentType, headers, (InputStream) null);
-               }
-
-               /**
-                * Creates a new response.
-                *
-                * @param statusCode
-                *            The HTTP status code of the response
-                * @param statusText
-                *            The HTTP status text of the response
-                * @param contentType
-                *            The content type of the response
-                * @param headers
-                *            The headers of the response
-                * @param content
-                *            The content of the reponse body
-                */
-               public Response(int statusCode, String statusText, String contentType, Map<String, String> headers, byte[] content) {
-                       this(statusCode, statusText, contentType, headers, new ByteArrayInputStream(content));
-               }
-
-               /**
-                * Creates a new response.
-                *
-                * @param statusCode
-                *            The HTTP status code of the response
-                * @param statusText
-                *            The HTTP status text of the response
-                * @param contentType
-                *            The content type of the response
-                * @param headers
-                *            The headers of the response
-                * @param content
-                *            The content of the reponse body
-                */
-               public Response(int statusCode, String statusText, String contentType, Map<String, String> headers, InputStream content) {
-                       this.statusCode = statusCode;
-                       this.statusText = statusText;
-                       this.contentType = contentType;
-                       this.headers = headers;
-                       this.content = content;
-               }
-
-               /**
-                * Returns the HTTP status code of the response.
-                *
-                * @return The HTTP status code
-                */
-               public int getStatusCode() {
-                       return statusCode;
-               }
-
-               /**
-                * Returns the HTTP status text.
-                *
-                * @return The HTTP status text
-                */
-               public String getStatusText() {
-                       return statusText;
-               }
-
-               /**
-                * Returns the content type of the response.
-                *
-                * @return The content type of the reponse
-                */
-               public String getContentType() {
-                       return contentType;
-               }
-
-               /**
-                * Returns HTTP headers of the response. May be {@code null} if no
-                * headers are returned.
-                *
-                * @return The response headers, or {@code null} if there are no
-                *         response headers
-                */
-               public Map<String, String> getHeaders() {
-                       return headers;
-               }
-
-               /**
-                * Sets the HTTP header with the given name to the given value. Multiple
-                * headers with the same name are not implemented so that latest call to
-                * {@link #setHeader(String, String)} determines what is sent to the
-                * browser.
-                *
-                * @param name
-                *            The name of the header
-                * @param value
-                *            The value of the header
-                * @return This response
-                */
-               public Response setHeader(String name, String value) {
-                       headers.put(name, value);
-                       return this;
-               }
-
-               /**
-                * Returns the content of the response body. May be {@code null} if the
-                * response does not have a body.
-                *
-                * @return The content of the response body
-                */
-               public InputStream getContent() {
-                       return content;
-               }
-
-               //
-               // PRIVATE METHODS
-               //
-
-               /**
-                * Returns the UTF-8 representation of the given text.
-                *
-                * @param text
-                *            The text to encode
-                * @return The encoded text
-                */
-               private static byte[] getBytes(String text) {
-                       try {
-                               return text.getBytes("UTF-8");
-                       } catch (UnsupportedEncodingException uee1) {
-                               /* every JVM needs to support UTF-8. */
-                       }
-                       return null;
-               }
-
-               /**
-                * Creates a header map containing a single header.
-                *
-                * @param name
-                *            The name of the header
-                * @param value
-                *            The value of the header
-                * @return The map containing the single header
-                */
-               protected static Map<String, String> createHeader(String name, String value) {
-                       Map<String, String> headers = new HashMap<String, String>();
-                       headers.put(name, value);
-                       return headers;
-               }
-
-       }
-
-       /**
-        * {@link Response} implementation that performs an HTTP redirect.
-        *
-        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
-        */
-       public class RedirectResponse extends Response {
-
-               /**
-                * Creates a new redirect response to the new location.
-                *
-                * @param newLocation
-                *            The new location
-                */
-               public RedirectResponse(String newLocation) {
-                       this(newLocation, true);
-               }
-
-               /**
-                * Creates a new redirect response to the new location.
-                *
-                * @param newLocation
-                *            The new location
-                * @param permanent
-                *            Whether the redirect should be marked as permanent
-                */
-               public RedirectResponse(String newLocation, boolean permanent) {
-                       super(permanent ? 302 : 307, "Redirected", null, createHeader("Location", newLocation));
-               }
-
-       }
-
-}
index f7f59a6..2d1f35a 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * shortener - PageToadlet.java - Copyright © 2010 David Roden
+ * Sone - PageToadlet.java - Copyright © 2010 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
@@ -19,9 +19,11 @@ package net.pterodactylus.sone.web.page;
 
 import java.io.IOException;
 import java.net.URI;
-import java.util.Map.Entry;
 
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.util.web.Header;
+import net.pterodactylus.util.web.Method;
+import net.pterodactylus.util.web.Page;
+import net.pterodactylus.util.web.Response;
 import freenet.client.HighLevelSimpleClient;
 import freenet.clients.http.LinkEnabledCallback;
 import freenet.clients.http.Toadlet;
@@ -30,7 +32,6 @@ import freenet.clients.http.ToadletContextClosedException;
 import freenet.support.MultiValueTable;
 import freenet.support.api.Bucket;
 import freenet.support.api.HTTPRequest;
-import freenet.support.io.BucketTools;
 import freenet.support.io.Closer;
 
 /**
@@ -44,7 +45,7 @@ public class PageToadlet extends Toadlet implements LinkEnabledCallback {
        private final String menuName;
 
        /** The page that handles processing. */
-       private final Page page;
+       private final Page<FreenetRequest> page;
 
        /** The path prefix for the page. */
        private final String pathPrefix;
@@ -62,7 +63,7 @@ public class PageToadlet extends Toadlet implements LinkEnabledCallback {
         *            Prefix that is prepended to all {@link Page#getPath()} return
         *            values
         */
-       protected PageToadlet(HighLevelSimpleClient highLevelSimpleClient, String menuName, Page page, String pathPrefix) {
+       protected PageToadlet(HighLevelSimpleClient highLevelSimpleClient, String menuName, Page<FreenetRequest> page, String pathPrefix) {
                super(highLevelSimpleClient);
                this.menuName = menuName;
                this.page = page;
@@ -101,7 +102,7 @@ public class PageToadlet extends Toadlet implements LinkEnabledCallback {
         *             if the toadlet context is closed
         */
        public void handleMethodGET(URI uri, HTTPRequest httpRequest, ToadletContext toadletContext) throws IOException, ToadletContextClosedException {
-               handleRequest(new Page.Request(uri, Method.GET, httpRequest, toadletContext));
+               handleRequest(new FreenetRequest(uri, Method.GET, httpRequest, toadletContext));
        }
 
        /**
@@ -119,7 +120,7 @@ public class PageToadlet extends Toadlet implements LinkEnabledCallback {
         *             if the toadlet context is closed
         */
        public void handleMethodPOST(URI uri, HTTPRequest httpRequest, ToadletContext toadletContext) throws IOException, ToadletContextClosedException {
-               handleRequest(new Page.Request(uri, Method.POST, httpRequest, toadletContext));
+               handleRequest(new FreenetRequest(uri, Method.POST, httpRequest, toadletContext));
        }
 
        /**
@@ -140,32 +141,25 @@ public class PageToadlet extends Toadlet implements LinkEnabledCallback {
         * @throws ToadletContextClosedException
         *             if the toadlet context is closed
         */
-       private void handleRequest(Page.Request pageRequest) throws IOException, ToadletContextClosedException {
-               Bucket data = null;
+       private void handleRequest(FreenetRequest pageRequest) throws IOException, ToadletContextClosedException {
+               Bucket pageBucket = null;
                try {
-                       Page.Response pageResponse = page.handleRequest(pageRequest);
+                       pageBucket = pageRequest.getToadletContext().getBucketFactory().makeBucket(-1);
+                       Response pageResponse = new Response(pageBucket.getOutputStream());
+                       pageResponse = page.handleRequest(pageRequest, pageResponse);
                        MultiValueTable<String, String> headers = new MultiValueTable<String, String>();
                        if (pageResponse.getHeaders() != null) {
-                               for (Entry<String, String> headerEntry : pageResponse.getHeaders().entrySet()) {
-                                       headers.put(headerEntry.getKey(), headerEntry.getValue());
+                               for (Header header : pageResponse.getHeaders()) {
+                                       for (String value : header) {
+                                               headers.put(header.getName(), value);
+                                       }
                                }
                        }
-                       data = pageRequest.getToadletContext().getBucketFactory().makeBucket(-1);
-                       if (pageResponse.getContent() != null) {
-                               try {
-                                       BucketTools.copyFrom(data, pageResponse.getContent(), -1);
-                               } finally {
-                                       Closer.close(pageResponse.getContent());
-                               }
-                       } else {
-                               /* get an OutputStream and close it immediately. */
-                               Closer.close(data.getOutputStream());
-                       }
-                       writeReply(pageRequest.getToadletContext(), pageResponse.getStatusCode(), pageResponse.getContentType(), pageResponse.getStatusText(), headers, data);
+                       writeReply(pageRequest.getToadletContext(), pageResponse.getStatusCode(), pageResponse.getContentType(), pageResponse.getStatusText(), headers, pageBucket);
                } catch (Throwable t1) {
                        writeInternalError(t1, pageRequest.getToadletContext());
                } finally {
-                       Closer.close(data);
+                       Closer.close(pageBucket);
                }
        }
 
index af87470..0abf3a5 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * shortener - PageToadletFactory.java - Copyright © 2010 David Roden
+ * Sone - PageToadletFactory.java - Copyright © 2010 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
 
 package net.pterodactylus.sone.web.page;
 
+import net.pterodactylus.util.web.Page;
 import freenet.client.HighLevelSimpleClient;
 
 /**
  * Factory that creates {@link PageToadlet}s using a given
  * {@link HighLevelSimpleClient}.
- * 
+ *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
 public class PageToadletFactory {
@@ -35,7 +36,7 @@ public class PageToadletFactory {
 
        /**
         * Creates a new {@link PageToadlet} factory.
-        * 
+        *
         * @param highLevelSimpleClient
         *            The client to use when creating the toadlets
         * @param pathPrefix
@@ -49,26 +50,26 @@ public class PageToadletFactory {
        /**
         * Creates a {@link PageToadlet} that wraps the given page and does not
         * appear in the node’s menu.
-        * 
+        *
         * @param page
         *            The page to wrap
         * @return The toadlet wrapped around the page
         */
-       public PageToadlet createPageToadlet(Page page) {
+       public PageToadlet createPageToadlet(Page<FreenetRequest> page) {
                return createPageToadlet(page, null);
        }
 
        /**
         * Creates a {@link PageToadlet} that wraps the given page and appears in
         * the node’s menu under the given name.
-        * 
+        *
         * @param page
         *            The page to wrap
         * @param menuName
         *            The name of the menu item
         * @return The toadlet wrapped around the page
         */
-       public PageToadlet createPageToadlet(Page page, String menuName) {
+       public PageToadlet createPageToadlet(Page<FreenetRequest> page, String menuName) {
                return new PageToadlet(highLevelSimpleClient, menuName, page, pathPrefix);
        }
 
diff --git a/src/main/java/net/pterodactylus/sone/web/page/RedirectPage.java b/src/main/java/net/pterodactylus/sone/web/page/RedirectPage.java
deleted file mode 100644 (file)
index 2ce34f9..0000000
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Sone - RedirectPage.java - Copyright © 2011 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.web.page;
-
-/**
- * Page implementation that redirects the user to another URL.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class RedirectPage implements Page {
-
-       /** The original path. */
-       private String originalPath;
-
-       /** The path to redirect the browser to. */
-       private String newPath;
-
-       /**
-        * Creates a new redirect page.
-        *
-        * @param originalPath
-        *            The original path
-        * @param newPath
-        *            The path to redirect the browser to
-        */
-       public RedirectPage(String originalPath, String newPath) {
-               this.originalPath = originalPath;
-               this.newPath = newPath;
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public String getPath() {
-               return originalPath;
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public Response handleRequest(Request request) {
-               return new RedirectResponse(newPath);
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/page/StaticPage.java b/src/main/java/net/pterodactylus/sone/web/page/StaticPage.java
deleted file mode 100644 (file)
index a840958..0000000
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * shortener - CSSPage.java - Copyright © 2010 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.web.page;
-
-import java.io.InputStream;
-
-/**
- * {@link Page} implementation that delivers static files from the class path.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class StaticPage implements Page {
-
-       /** The prefix for {@link #getPath()}. */
-       private final String pathPrefix;
-
-       /** The path used as prefix when loading resources. */
-       private final String resourcePathPrefix;
-
-       /** The MIME type for the files this path contains. */
-       private final String mimeType;
-
-       /**
-        * Creates a new CSS page.
-        *
-        * @param pathPrefix
-        *            The prefix for {@link #getPath()}
-        * @param resourcePathPrefix
-        *            The path prefix when loading resources
-        * @param mimeType
-        *            The MIME type of the files this path contains
-        */
-       public StaticPage(String pathPrefix, String resourcePathPrefix, String mimeType) {
-               this.pathPrefix = pathPrefix;
-               this.resourcePathPrefix = resourcePathPrefix;
-               this.mimeType = mimeType;
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public String getPath() {
-               return pathPrefix;
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public Response handleRequest(Request request) {
-               String path = request.getUri().getPath();
-               int lastSlash = path.lastIndexOf('/');
-               String filename = path.substring(lastSlash + 1);
-               InputStream fileInputStream = getClass().getResourceAsStream(resourcePathPrefix + filename);
-               if (fileInputStream == null) {
-                       return new Response(404, "Not found.", null, "");
-               }
-               return new Response(200, "OK", mimeType, null, fileInputStream);
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/page/TemplatePage.java b/src/main/java/net/pterodactylus/sone/web/page/TemplatePage.java
deleted file mode 100644 (file)
index 06fc9fc..0000000
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- * Sone - StaticTemplatePage.java - Copyright © 2011 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.web.page;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.OutputStreamWriter;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-import net.pterodactylus.util.io.Closer;
-import net.pterodactylus.util.logging.Logging;
-import net.pterodactylus.util.template.Template;
-import net.pterodactylus.util.template.TemplateContext;
-import net.pterodactylus.util.template.TemplateContextFactory;
-
-/**
- * A template page is a single page that is created from a {@link Template} but
- * does not necessarily return HTML.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class TemplatePage implements Page {
-
-       /** The logger. */
-       private static final Logger logger = Logging.getLogger(TemplatePage.class);
-
-       /** The path of this page. */
-       private final String path;
-
-       /** The content type of this page. */
-       private final String contentType;
-
-       /** The template context factory. */
-       private final TemplateContextFactory templateContextFactory;
-
-       /** The template to render. */
-       private final Template template;
-
-       /**
-        * Creates a new template page.
-        *
-        * @param path
-        *            The path of the page
-        * @param contentType
-        *            The content type of the page
-        * @param templateContextFactory
-        *            The template context factory
-        * @param template
-        *            The template to render
-        */
-       public TemplatePage(String path, String contentType, TemplateContextFactory templateContextFactory, Template template) {
-               this.path = path;
-               this.contentType = contentType;
-               this.templateContextFactory = templateContextFactory;
-               this.template = template;
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public String getPath() {
-               return path;
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public Response handleRequest(Request request) {
-               ByteArrayOutputStream responseOutputStream = new ByteArrayOutputStream();
-               OutputStreamWriter responseWriter = null;
-               try {
-                       responseWriter = new OutputStreamWriter(responseOutputStream, "UTF-8");
-                       TemplateContext templateContext = templateContextFactory.createTemplateContext();
-                       templateContext.set("request", request);
-                       template.render(templateContext, responseWriter);
-               } catch (IOException ioe1) {
-                       logger.log(Level.WARNING, "Could not render template for path “" + path + "”!", ioe1);
-               } finally {
-                       Closer.close(responseWriter);
-                       Closer.close(responseOutputStream);
-               }
-               ByteArrayInputStream responseInputStream = new ByteArrayInputStream(responseOutputStream.toByteArray());
-               /* no need to close a ByteArrayInputStream. */
-               return new Response(200, "OK", contentType, null, responseInputStream);
-       }
-
-}
index 5f0a9ee..51cbe9d 100644 (file)
@@ -20,6 +20,8 @@ Navigation.Menu.Item.Logout.Name=Logout
 Navigation.Menu.Item.Logout.Tooltip=Logs you out of the current Sone
 Navigation.Menu.Item.Options.Name=Options
 Navigation.Menu.Item.Options.Tooltip=Options for the Sone plugin
+Navigation.Menu.Item.Rescue.Name=Rescue
+Navigation.Menu.Item.Rescue.Tooltip=Rescue Sone
 Navigation.Menu.Item.About.Name=About
 Navigation.Menu.Item.About.Tooltip=Information about Sone
 
@@ -36,19 +38,27 @@ Page.Options.Page.Description=These options influence the runtime behaviour of t
 Page.Options.Section.SoneSpecificOptions.Title=Sone-specific Options
 Page.Options.Section.SoneSpecificOptions.NotLoggedIn=These options are only available if you are {link}logged in{/link}.
 Page.Options.Section.SoneSpecificOptions.LoggedIn=These options are only available while you are logged in and they are only valid for the Sone you are logged in as.
-Page.Options.Option.AutoFollow.Description=If a new Sone is discovered, follow it automatically.
+Page.Options.Option.AutoFollow.Description=If a new Sone is discovered, follow it automatically. Note that this will only follow Sones that are discovered after you activate this option!
+Page.Options.Option.EnableSoneInsertNotifications.Description=If enabled, this will display notifications every time your Sone is being inserted or finishes inserting.
 Page.Options.Section.RuntimeOptions.Title=Runtime Behaviour
 Page.Options.Option.InsertionDelay.Description=The number of seconds the Sone inserter waits after a modification of a Sone before it is being inserted.
 Page.Options.Option.PostsPerPage.Description=The number of posts to display on a page before pagination controls are being shown.
+Page.Options.Option.CharactersPerPost.Description=The number of characters to display from a post before cutting it off and showing a link to expand it (-1 to disable).
+Page.Options.Option.RequireFullAccess.Description=Whether to deny access to Sone to any host that has not been granted full access.
 Page.Options.Section.TrustOptions.Title=Trust Settings
 Page.Options.Option.PositiveTrust.Description=The amount of positive trust you want to assign to other Sones by clicking the checkmark below a post or reply.
 Page.Options.Option.NegativeTrust.Description=The amount of trust you want to assign to other Sones by clicking the red X below a post or reply. This value should be negative.
 Page.Options.Option.TrustComment.Description=The comment that will be set in the web of trust for any trust you assign from Sone.
-Page.Options.Section.RescueOptions.Title=Rescue Settings
-Page.Options.Option.SoneRescueMode.Description=Try to rescue your Sones at the next start of the Sone plugin. This will read your all your old Sones from Freenet and ignore any disappearing postings and replies. You have to unlock your local Sones after they have been restored and you have to manually disable the rescue mode once you are satisfied with what has been restored!
+Page.Options.Section.FcpOptions.Title=FCP Interface Settings
+Page.Options.Option.FcpInterfaceActive.Description=Activate the FCP interface to allow other plugins and remote clients to access your Sone plugin.
+Page.Options.Option.FcpFullAccessRequired.Description=Require FCP connection from allowed hosts (see your {link}node’s configuration, section “FCP”{/link})
+Page.Options.Option.FcpFullAccessRequired.Value.No=No
+Page.Options.Option.FcpFullAccessRequired.Value.Writing=For Write Access
+Page.Options.Option.FcpFullAccessRequired.Value.Always=Always
 Page.Options.Section.Cleaning.Title=Clean Up
 Page.Options.Option.ClearOnNextRestart.Description=Resets the configuration of the Sone plugin at the next restart. Warning! {strong}This will destroy all of your Sones{/strong} so make sure you have backed up everyhing you still need! Also, you need to set the next option to true to actually do it.
 Page.Options.Option.ReallyClearOnNextRestart.Description=This option needs to be set to “yes” if you really, {strong}really{/strong} want to clear the plugin configuration on the next restart.
+Page.Options.Warnings.ValueNotChanged=This option was not changed because the value you specified was not valid.
 Page.Options.Button.Save=Save
 
 Page.Login.Title=Login - Sone
@@ -76,6 +86,8 @@ Page.Index.PostList.Text.NoPostYet=Nobody has written any posts yet. You should
 Page.KnownSones.Title=Known Sones - Sone
 Page.KnownSones.Page.Title=Known Sones
 Page.KnownSones.Text.NoKnownSones=There are currently no known Sones.
+Page.KnownSones.Button.FollowAllSones=Follow all Sones
+Page.KnownSones.Button.UnfollowAllSones=Unfollow all Sones
 
 Page.EditProfile.Title=Edit Profile - Sone
 Page.EditProfile.Page.Title=Edit Profile
@@ -131,13 +143,15 @@ Page.ViewSone.Title=View Sone - Sone
 Page.ViewSone.Page.TitleWithoutSone=View unknown Sone
 Page.ViewSone.NoSone.Description=There is currently no known Sone with the ID {sone}. If you were looking for a specific Sone, make sure that it is visible in your web of trust!
 Page.ViewSone.UnknownSone.Description=This Sone has not yet been retrieved. Please check back in a short time.
+Page.ViewSone.UnknownSone.LinkToWebOfTrust=Even though the Sone is still unknown, its Web of Trust profile might already be available:
 Page.ViewSone.WriteAMessage=You can write a message to this Sone here. Please note that everybody will be able to read this message!
 Page.ViewSone.PostList.Title=Posts by {sone}
 Page.ViewSone.PostList.Text.NoPostYet=This Sone has not yet posted anything.
 Page.ViewSone.Profile.Title=Profile
 Page.ViewSone.Profile.Label.Name=Name
 Page.ViewSone.Profile.Label.Albums=Albums
-Page.ViewSone.Replies.Title=Replies to Posts
+Page.ViewSone.Profile.Name.WoTLink=web of trust profile
+Page.ViewSone.Replies.Title=Posts {sone} has replied to
 
 Page.ViewPost.Title=View Post - Sone
 Page.ViewPost.Page.Title=View Post by {sone}
@@ -229,6 +243,17 @@ Page.Search.Text.SoneHits=The following Sones match your search terms.
 Page.Search.Text.PostHits=The following posts match your search terms.
 Page.Search.Text.NoHits=No Sones or posts matched your search terms.
 
+Page.Rescue.Title=Rescue Sone
+Page.Rescue.Page.Title=Rescue Sone “{0}”
+Page.Rescue.Text.Description=The Rescue Mode lets you restore previous versions of your Sone. This can be necessary if your configuration was lost.
+Page.Rescue.Text.Procedure=The Rescue Mode works by fetching the latest inserted edition of your Sone. If an edition was successfully fetched it will be loaded into your Sone, letting you control your posts, profile, and other settings (you could do that in a second browser tab or window). If the fetched edition is not the one you want to restore, instruct the Rescue Mode to fetch the next older edition below.
+Page.Rescue.Text.Fetching=The Sone Rescuer is currently fetching edition {0} of your Sone.
+Page.Rescue.Text.Fetched=The Sone Rescuer has downloaded edition {0} of your Sone. Please check your posts, replies, and profile. If you like what the current Sone contains, just unlock it.
+Page.Rescue.Text.FetchedLast=The Sone rescuer has downloaded the last available edition. If it did not manage to restore your Sone you are probably out of luck now.
+Page.Rescue.Text.NotFetched=The Sone Rescuer could not download edition {0} of your Sone. Please either try again with edition {0}, or try the next older edition.
+Page.Rescue.Label.NextEdition=Next edition:
+Page.Rescue.Button.Fetch=Fetch edition
+
 Page.NoPermission.Title=Unauthorized Access - Sone
 Page.NoPermission.Page.Title=Unauthorized Access
 Page.NoPermission.Text.NoPermission=You tried to do something that you do not have sufficient authorization for. Please refrain from such actions in the future or we will be forced to take counter-measures!
@@ -255,6 +280,8 @@ View.CreateSone.Text.Error.NoIdentity=You have not selected an identity.
 
 View.Sone.Label.LastUpdate=Last update:
 View.Sone.Text.UnknownDate=unknown
+View.Sone.Stats.Posts={0,number} {0,choice,0#posts|1#post|1<posts}
+View.Sone.Stats.Replies={0,number} {0,choice,0#replies|1#reply|1<replies}
 View.Sone.Button.UnlockSone=unlock
 View.Sone.Button.UnlockSone.Tooltip=Allow this Sone to be inserted now
 View.Sone.Button.LockSone=lock
@@ -268,6 +295,9 @@ View.Sone.Status.Downloading=This Sone is currently being downloaded.
 View.Sone.Status.Inserting=This Sone is currently being inserted.
 
 View.Post.UnknownAuthor=(unknown)
+View.Post.WebOfTrustLink=web of trust profile
+View.Post.Permalink=link post
+View.Post.PermalinkAuthor=link author
 View.Post.Bookmarks.PostIsBookmarked=Post is bookmarked, click to remove from bookmarks
 View.Post.Bookmarks.PostIsNotBookmarked=Post is not bookmarked, click to bookmark
 View.Post.DeleteLink=Delete
@@ -277,6 +307,8 @@ View.Post.LikeLink=Like
 View.Post.UnlikeLink=Unlike
 View.Post.ShowSource=Toggle Parser
 View.Post.NotDownloaded=This post has not yet been downloaded, or it has been deleted.
+View.Post.ShowMore=show more
+View.Post.ShowLess=show less
 
 View.UpdateStatus.Text.ChooseSenderIdentity=Choose the sender identity
 
@@ -304,7 +336,7 @@ View.Time.XHoursAgo=${hour} hours ago
 View.Time.ADayAgo=about a day ago
 View.Time.XDaysAgo=${day} days ago
 View.Time.AWeekAgo=about a week ago
-View.Time.XWeeksAgo=${week} week ago
+View.Time.XWeeksAgo=${week} weeks ago
 View.Time.AMonthAgo=about a month ago
 View.Time.XMonthsAgo=${month} months ago
 View.Time.AYearAgo=about a year ago
@@ -331,6 +363,7 @@ WebInterface.DefaultText.UploadImage.Description=Image description
 WebInterface.DefaultText.EditImage.Title=Image title
 WebInterface.DefaultText.EditImage.Description=Image description
 WebInterface.DefaultText.Option.PostsPerPage=Number of posts to show on a page
+WebInterface.DefaultText.Option.CharactersPerPost=Number of characters per post after which to cut the post off
 WebInterface.DefaultText.Option.PositiveTrust=The positive trust to assign
 WebInterface.DefaultText.Option.NegativeTrust=The negative trust to assign
 WebInterface.DefaultText.Option.TrustComment=The comment to set in the web of trust
@@ -363,3 +396,6 @@ Notification.NewVersion.Text=Version {version} of the Sone plugin was found. Dow
 Notification.InsertingImages.Text=The following images are being inserted:
 Notification.InsertedImages.Text=The following images have been inserted:
 Notification.ImageInsertFailed.Text=The following images could not be inserted:
+Notification.Mention.ShortText=You have been mentioned.
+Notification.Mention.Text=You have been mentioned in the following posts:
+Notification.SoneInsert.Duration={0,number} {0,choice,0#seconds|1#second|1<seconds}
index 9470850..3429138 100644 (file)
@@ -89,6 +89,10 @@ textarea {
        content: '★ ';
 }
 
+#sone a.in-page-link:before {
+       content: '↓ ';
+}
+
 #sone a img {
        border: none;
 }
@@ -97,6 +101,20 @@ textarea {
        white-space: pre-wrap;
 }
 
+#sone #main.offline {
+       opacity: 0.5;
+}
+
+#sone #offline-marker {
+       display: none;
+       position: fixed;
+       top: 2em;
+       right: 2em;
+       width: 128px;
+       height: 128px;
+       background-image: url("../images/sone-offline.png");
+}
+
 #sone #notification-area {
        margin-top: 1em;
 }
@@ -129,6 +147,10 @@ textarea {
        display: none;
 }
 
+#sone #notification-area #local-post-notification, #sone #notification-area #local-reply-notification {
+       display: none;
+}
+
 #sone #plugin-warning {
        border: solid 0.5em red;
        padding: 0.5em;
@@ -220,6 +242,7 @@ textarea {
        padding: 1ex 0px;
        border-bottom: solid 1px #ccc;
        clear: both;
+       position: relative;
 }
 
 #sone .post.new {
@@ -232,6 +255,33 @@ textarea {
        border-bottom: none;
 }
 
+#sone .post .sone-menu {
+       position: absolute;
+       top: 0;
+       left: -1ex;
+       padding: 1ex 1ex;
+       margin: -1px -1px;
+       display: none;
+       background-color: rgb(255, 255, 224);
+       border: solid 1px rgb(0, 0, 0);
+       z-index: 1;
+}
+
+#sone .post .sone-menu .avatar {
+       position: absolute;
+       margin-right: 1ex;
+}
+
+#sone .post .sone-menu .inner-menu {
+       margin-left: 64px;
+       padding-left: 1ex;
+       min-height: 64px;
+}
+
+#sone .sone-menu .follow, #sone .sone-menu .unfollow {
+       cursor: pointer;
+}
+
 #sone .post > .avatar {
        position: absolute;
 }
@@ -247,15 +297,32 @@ textarea {
        font-weight: bold;
 }
 
-#sone .post .text, #sone .post .raw-text {
+#sone .post .author-wot-link {
+       font-size: 90%;
+}
+
+#sone .post .text, #sone .post .raw-text, #sone .post .short-text {
        display: inline;
        white-space: pre-wrap;
+       word-wrap: break-word;
 }
 
-#sone .post .text.hidden, #sone .post .raw-text.hidden {
+#sone .post .text.hidden, #sone .post .raw-text.hidden, #sone .post .short-text.hidden {
        display: none;
 }
 
+#sone .post .expand-post-text:before, #sone .post .expand-reply-text:before {
+       content: "» ";
+}
+
+#sone .post .shrink-post-text:before, #sone .post .shrink-reply-text:before {
+       content: "« ";
+}
+
+#sone .post .shrink-post-text {
+       cursor: pointer;
+}
+
 #sone .post .status-line {
        margin-top: 0.5ex;
        font-size: 85%;
@@ -270,6 +337,10 @@ textarea {
        display: inline;
 }
 
+#sone .permalink {
+       display: inline;
+}
+
 #sone .post .bookmarks {
        display: inline;
        color: rgb(28, 131, 191);
@@ -366,13 +437,17 @@ textarea {
 }
 
 #sone .post .reply {
+       position: relative;
        clear: both;
        background-color: #f0f0ff;
-       font-size: 85%;
        margin: 1ex 0px;
        padding: 1ex;
 }
 
+#sone .post .reply .inner-part {
+       font-size: 85%;
+}
+
 #sone .post .reply.new {
        background-color: #ffffa0;
 }
@@ -441,7 +516,11 @@ textarea {
 }
 
 #sone .sone .profile-link {
-       display: block;
+       display: inline;
+}
+
+#sone .sone .sone-stats {
+       display: inline;
 }
 
 #sone .sone .short-request-uri {
@@ -682,3 +761,12 @@ textarea {
        font-weight: bold;
        color: red;
 }
+
+#sone .warning {
+       color: red;
+       font-style: italic;
+}
+
+#sone #sort-options {
+       margin-bottom: 1em;
+}
diff --git a/src/main/resources/static/images/sone-offline.png b/src/main/resources/static/images/sone-offline.png
new file mode 100644 (file)
index 0000000..18430bf
Binary files /dev/null and b/src/main/resources/static/images/sone-offline.png differ
diff --git a/src/main/resources/static/javascript/jquery.fieldselection.js b/src/main/resources/static/javascript/jquery.fieldselection.js
new file mode 100644 (file)
index 0000000..47f25a9
--- /dev/null
@@ -0,0 +1,83 @@
+/*
+ * jQuery plugin: fieldSelection - v0.1.0 - last change: 2006-12-16
+ * (c) 2006 Alex Brem <alex@0xab.cd> - http://blog.0xab.cd
+ */
+
+(function() {
+
+       var fieldSelection = {
+
+               getSelection: function() {
+
+                       var e = this.jquery ? this[0] : this;
+
+                       return (
+
+                               /* mozilla / dom 3.0 */
+                               ('selectionStart' in e && function() {
+                                       var l = e.selectionEnd - e.selectionStart;
+                                       return { start: e.selectionStart, end: e.selectionEnd, length: l, text: e.value.substr(e.selectionStart, l) };
+                               }) ||
+
+                               /* exploder */
+                               (document.selection && function() {
+
+                                       e.focus();
+
+                                       var r = document.selection.createRange();
+                                       if (r == null) {
+                                               return { start: 0, end: e.value.length, length: 0 }
+                                       }
+
+                                       var re = e.createTextRange();
+                                       var rc = re.duplicate();
+                                       re.moveToBookmark(r.getBookmark());
+                                       rc.setEndPoint('EndToStart', re);
+
+                                       return { start: rc.text.length, end: rc.text.length + r.text.length, length: r.text.length, text: r.text };
+                               }) ||
+
+                               /* browser not supported */
+                               function() {
+                                       return { start: 0, end: e.value.length, length: 0 };
+                               }
+
+                       )();
+
+               },
+
+               replaceSelection: function() {
+
+                       var e = this.jquery ? this[0] : this;
+                       var text = arguments[0] || '';
+
+                       return (
+
+                               /* mozilla / dom 3.0 */
+                               ('selectionStart' in e && function() {
+                                       e.value = e.value.substr(0, e.selectionStart) + text + e.value.substr(e.selectionEnd, e.value.length);
+                                       return this;
+                               }) ||
+
+                               /* exploder */
+                               (document.selection && function() {
+                                       e.focus();
+                                       document.selection.createRange().text = text;
+                                       return this;
+                               }) ||
+
+                               /* browser not supported */
+                               function() {
+                                       e.value += text;
+                                       return this;
+                               }
+
+                       )();
+
+               }
+
+       };
+
+       jQuery.each(fieldSelection, function(i) { jQuery.fn[i] = this; });
+
+})();
index d58f17f..f6327e4 100644 (file)
@@ -1,26 +1,23 @@
 /* Sone JavaScript functions. */
 
-/* jQuery overrides. */
-oldGetJson = jQuery.prototype.getJSON;
-jQuery.prototype.getJSON = function(url, data, successCallback, errorCallback) {
-       if (typeof errorCallback == "undefined") {
-               return oldGetJson(url, data, successCallback);
-       }
-       if (jQuery.isFunction(data)) {
-               errorCallback = successCallback;
-               successCallback = data;
-               data = null;
-       }
-       return jQuery.ajax({
-               data: data,
-               error: errorCallback,
-               success: successCallback,
-               url: url
-       });
-}
-
-function isOnline() {
-       return $("#sone").hasClass("online");
+function ajaxGet(url, data, successCallback, errorCallback) {
+       (function(url, data, successCallback, errorCallback) {
+               $.ajax({"type": "GET", "url": url, "data": data, "dataType": "json", "success": function(data, textStatus, xmlHttpRequest) {
+                       ajaxSuccess();
+                       if (typeof successCallback != "undefined") {
+                               successCallback(data, textStatus);
+                       }
+               }, "error": function(xmlHttpRequest, textStatus, errorThrown) {
+                       if (xmlHttpRequest.status == 403) {
+                               notLoggedIn = true;
+                       }
+                       if (typeof errorCallback != "undefined") {
+                               errorCallback();
+                       } else {
+                               ajaxError();
+                       }
+               }});
+       })(url, data, successCallback, errorCallback);
 }
 
 function registerInputTextareaSwap(inputElement, defaultText, inputFieldName, optional, dontUseTextarea) {
@@ -86,7 +83,9 @@ function addCommentLink(postId, author, element, insertAfterThisElement) {
                                });
                        })(replyElement);
                        textArea = replyElement.find("input.reply-input").focus().data("textarea");
-                       textArea.val(textArea.val() + "@sone://" + author + " ");
+                       if (author != getCurrentSoneId()) {
+                               textArea.val(textArea.val() + "@sone://" + author + " ");
+                       }
                });
                return commentElement;
        })(postId, author);
@@ -110,13 +109,11 @@ function getTranslation(key, callback) {
                callback(translations[key]);
                return;
        }
-       $.getJSON("getTranslation.ajax", {"key": key}, function(data, textStatus) {
+       ajaxGet("getTranslation.ajax", {"key": key}, function(data, textStatus) {
                if ((data != null) && data.success) {
                        translations[key] = data.value;
                        callback(data.value);
                }
-       }, function(xmlHttpRequest, textStatus, error) {
-               /* ignore error. */
        });
 }
 
@@ -146,7 +143,7 @@ function filterSoneId(soneId) {
  * @param lastUpdated
  *            The date and time of the last update (formatted for display)
  */
-function updateSoneStatus(soneId, name, status, modified, locked, lastUpdated) {
+function updateSoneStatus(soneId, name, status, modified, locked, lastUpdated, lastUpdatedText) {
        $("#sone .sone." + filterSoneId(soneId)).
                toggleClass("unknown", status == "unknown").
                toggleClass("idle", status == "idle").
@@ -156,7 +153,7 @@ function updateSoneStatus(soneId, name, status, modified, locked, lastUpdated) {
        $("#sone .sone." + filterSoneId(soneId) + " .lock").toggleClass("hidden", locked);
        $("#sone .sone." + filterSoneId(soneId) + " .unlock").toggleClass("hidden", !locked);
        if (lastUpdated != null) {
-               $("#sone .sone." + filterSoneId(soneId) + " .last-update span.time").text(lastUpdated);
+               $("#sone .sone." + filterSoneId(soneId) + " .last-update span.time").attr("title", lastUpdated).text(lastUpdatedText);
        } else {
                getTranslation("View.Sone.Text.UnknownDate", function(unknown) {
                        $("#sone .sone." + filterSoneId(soneId) + " .last-update span.time").text(unknown);
@@ -212,7 +209,7 @@ function enhanceDeleteButton(button, text, deleteCallback) {
  */
 function enhanceDeletePostButton(button, postId, text) {
        enhanceDeleteButton(button, text, function() {
-               $.getJSON("deletePost.ajax", { "post": postId, "formPassword": getFormPassword() }, function(data, textStatus) {
+               ajaxGet("deletePost.ajax", { "post": postId, "formPassword": getFormPassword() }, function(data, textStatus) {
                        if (data == null) {
                                return;
                        }
@@ -244,7 +241,7 @@ function enhanceDeletePostButton(button, postId, text) {
  */
 function enhanceDeleteReplyButton(button, replyId, text) {
        enhanceDeleteButton(button, text, function() {
-               $.getJSON("deleteReply.ajax", { "reply": replyId, "formPassword": $("#sone #formPassword").text() }, function(data, textStatus) {
+               ajaxGet("deleteReply.ajax", { "reply": replyId, "formPassword": $("#sone #formPassword").text() }, function(data, textStatus) {
                        if (data == null) {
                                return;
                        }
@@ -277,7 +274,7 @@ function getFormPassword() {
  */
 function getSone(soneId) {
        return $("#sone .sone").filter(function(index) {
-               return $(".id").text() == soneId;
+               return $(".id", this).text() == soneId;
        });
 }
 
@@ -286,6 +283,18 @@ function getSoneElement(element) {
 }
 
 /**
+ * Returns the ID of the sone of the context menu that contains the given
+ * element.
+ *
+ * @param element
+ *            The element within a context menu to get the Sone ID for
+ * @return The Sone ID
+ */
+function getMenuSone(element) {
+       return $(element).closest(".sone-menu").find(".sone-menu-id").text();
+}
+
+/**
  * Generates a list of Sones by concatening the names of the given sones with a
  * new line character (“\n”).
  *
@@ -416,8 +425,19 @@ function getNotificationId(notificationElement) {
        return $(notificationElement).attr("id");
 }
 
+/**
+ * Returns the time the notification was last updated.
+ *
+ * @param notificationElement
+ *            The notification element
+ * @returns The last update time of the notification
+ */
+function getNotificationLastUpdatedTime(notificationElement) {
+       return $(notificationElement).attr("lastUpdatedTime");
+}
+
 function likePost(postId) {
-       $.getJSON("like.ajax", { "type": "post", "post" : postId, "formPassword": getFormPassword() }, function(data, textStatus) {
+       ajaxGet("like.ajax", { "type": "post", "post" : postId, "formPassword": getFormPassword() }, function(data, textStatus) {
                if ((data == null) || !data.success) {
                        return;
                }
@@ -430,7 +450,7 @@ function likePost(postId) {
 }
 
 function unlikePost(postId) {
-       $.getJSON("unlike.ajax", { "type": "post", "post" : postId, "formPassword": getFormPassword() }, function(data, textStatus) {
+       ajaxGet("unlike.ajax", { "type": "post", "post" : postId, "formPassword": getFormPassword() }, function(data, textStatus) {
                if ((data == null) || !data.success) {
                        return;
                }
@@ -443,9 +463,9 @@ function unlikePost(postId) {
 }
 
 function updatePostLikes(postId) {
-       $.getJSON("getLikes.ajax", { "type": "post", "post": postId }, function(data, textStatus) {
+       ajaxGet("getLikes.ajax", { "type": "post", "post": postId }, function(data, textStatus) {
                if ((data != null) && data.success) {
-                       $("#sone .post#" + postId + " > .inner-part > .status-line .likes").toggleClass("hidden", data.likes == 0)
+                       $("#sone .post#" + postId + " > .inner-part > .status-line .likes").toggleClass("hidden", data.likes == 0);
                        $("#sone .post#" + postId + " > .inner-part > .status-line .likes span.like-count").text(data.likes);
                        $("#sone .post#" + postId + " > .inner-part > .status-line .likes > span").attr("title", generateSoneList(data.sones));
                }
@@ -455,7 +475,7 @@ function updatePostLikes(postId) {
 }
 
 function likeReply(replyId) {
-       $.getJSON("like.ajax", { "type": "reply", "reply" : replyId, "formPassword": getFormPassword() }, function(data, textStatus) {
+       ajaxGet("like.ajax", { "type": "reply", "reply" : replyId, "formPassword": getFormPassword() }, function(data, textStatus) {
                if ((data == null) || !data.success) {
                        return;
                }
@@ -468,7 +488,7 @@ function likeReply(replyId) {
 }
 
 function unlikeReply(replyId) {
-       $.getJSON("unlike.ajax", { "type": "reply", "reply" : replyId, "formPassword": getFormPassword() }, function(data, textStatus) {
+       ajaxGet("unlike.ajax", { "type": "reply", "reply" : replyId, "formPassword": getFormPassword() }, function(data, textStatus) {
                if ((data == null) || !data.success) {
                        return;
                }
@@ -487,7 +507,7 @@ function unlikeReply(replyId) {
  *            The ID of the Sone to trust
  */
 function trustSone(soneId) {
-       $.getJSON("trustSone.ajax", { "formPassword" : getFormPassword(), "sone" : soneId }, function(data, textStatus) {
+       ajaxGet("trustSone.ajax", { "formPassword" : getFormPassword(), "sone" : soneId }, function(data, textStatus) {
                if ((data != null) && data.success) {
                        updateTrustControls(soneId, data.trustValue);
                }
@@ -501,7 +521,7 @@ function trustSone(soneId) {
  *            The ID of the Sone to distrust
  */
 function distrustSone(soneId) {
-       $.getJSON("distrustSone.ajax", { "formPassword" : getFormPassword(), "sone" : soneId }, function(data, textStatus) {
+       ajaxGet("distrustSone.ajax", { "formPassword" : getFormPassword(), "sone" : soneId }, function(data, textStatus) {
                if ((data != null) && data.success) {
                        updateTrustControls(soneId, data.trustValue);
                }
@@ -515,7 +535,7 @@ function distrustSone(soneId) {
  *            The ID of the Sone to untrust
  */
 function untrustSone(soneId) {
-       $.getJSON("untrustSone.ajax", { "formPassword" : getFormPassword(), "sone" : soneId }, function(data, textStatus) {
+       ajaxGet("untrustSone.ajax", { "formPassword" : getFormPassword(), "sone" : soneId }, function(data, textStatus) {
                if ((data != null) && data.success) {
                        updateTrustControls(soneId, data.trustValue);
                }
@@ -556,7 +576,7 @@ function updateTrustControls(soneId, trustValue) {
  */
 function bookmarkPost(postId) {
        (function(postId) {
-               $.getJSON("bookmark.ajax", {"formPassword": getFormPassword(), "type": "post", "post": postId}, function(data, textStatus) {
+               ajaxGet("bookmark.ajax", {"formPassword": getFormPassword(), "type": "post", "post": postId}, function(data, textStatus) {
                        if ((data != null) && data.success) {
                                getPost(postId).find(".bookmark").toggleClass("hidden", true);
                                getPost(postId).find(".unbookmark").toggleClass("hidden", false);
@@ -572,7 +592,7 @@ function bookmarkPost(postId) {
  *            The ID of the post to unbookmark
  */
 function unbookmarkPost(postId) {
-       $.getJSON("unbookmark.ajax", {"formPassword": getFormPassword(), "type": "post", "post": postId}, function(data, textStatus) {
+       ajaxGet("unbookmark.ajax", {"formPassword": getFormPassword(), "type": "post", "post": postId}, function(data, textStatus) {
                if ((data != null) && data.success) {
                        getPost(postId).find(".bookmark").toggleClass("hidden", false);
                        getPost(postId).find(".unbookmark").toggleClass("hidden", true);
@@ -581,9 +601,9 @@ function unbookmarkPost(postId) {
 }
 
 function updateReplyLikes(replyId) {
-       $.getJSON("getLikes.ajax", { "type": "reply", "reply": replyId }, function(data, textStatus) {
+       ajaxGet("getLikes.ajax", { "type": "reply", "reply": replyId }, function(data, textStatus) {
                if ((data != null) && data.success) {
-                       $("#sone .reply#" + replyId + " .status-line .likes").toggleClass("hidden", data.likes == 0)
+                       $("#sone .reply#" + replyId + " .status-line .likes").toggleClass("hidden", data.likes == 0);
                        $("#sone .reply#" + replyId + " .status-line .likes span.like-count").text(data.likes);
                        $("#sone .reply#" + replyId + " .status-line .likes > span").attr("title", generateSoneList(data.sones));
                }
@@ -606,7 +626,7 @@ function updateReplyLikes(replyId) {
  *            parameters: success, error, replyId)
  */
 function postReply(sender, postId, text, callbackFunction) {
-       $.getJSON("createReply.ajax", { "formPassword" : getFormPassword(), "sender": sender, "post" : postId, "text": text }, function(data, textStatus) {
+       ajaxGet("createReply.ajax", { "formPassword" : getFormPassword(), "sender": sender, "post" : postId, "text": text }, function(data, textStatus) {
                if (data == null) {
                        /* TODO - show error */
                        return;
@@ -634,7 +654,7 @@ function ajaxifySone(soneElement) {
         */
        $(".follow", soneElement).submit(function() {
                var followElement = this;
-               $.getJSON("followSone.ajax", { "sone": getSoneId(this), "formPassword": getFormPassword() }, function() {
+               ajaxGet("followSone.ajax", { "sone": getSoneId(this), "formPassword": getFormPassword() }, function() {
                        $(followElement).addClass("hidden");
                        $(followElement).parent().find(".unfollow").removeClass("hidden");
                });
@@ -642,7 +662,7 @@ function ajaxifySone(soneElement) {
        });
        $(".unfollow", soneElement).submit(function() {
                var unfollowElement = this;
-               $.getJSON("unfollowSone.ajax", { "sone": getSoneId(this), "formPassword": getFormPassword() }, function() {
+               ajaxGet("unfollowSone.ajax", { "sone": getSoneId(this), "formPassword": getFormPassword() }, function() {
                        $(unfollowElement).addClass("hidden");
                        $(unfollowElement).parent().find(".follow").removeClass("hidden");
                });
@@ -650,7 +670,7 @@ function ajaxifySone(soneElement) {
        });
        $(".lock", soneElement).submit(function() {
                var lockElement = this;
-               $.getJSON("lockSone.ajax", { "sone" : getSoneId(this), "formPassword" : getFormPassword() }, function() {
+               ajaxGet("lockSone.ajax", { "sone" : getSoneId(this), "formPassword" : getFormPassword() }, function() {
                        $(lockElement).addClass("hidden");
                        $(lockElement).parent().find(".unlock").removeClass("hidden");
                });
@@ -658,7 +678,7 @@ function ajaxifySone(soneElement) {
        });
        $(".unlock", soneElement).submit(function() {
                var unlockElement = this;
-               $.getJSON("unlockSone.ajax", { "sone" : getSoneId(this), "formPassword" : getFormPassword() }, function() {
+               ajaxGet("unlockSone.ajax", { "sone" : getSoneId(this), "formPassword" : getFormPassword() }, function() {
                        $(unlockElement).addClass("hidden");
                        $(unlockElement).parent().find(".lock").removeClass("hidden");
                });
@@ -751,14 +771,55 @@ function ajaxifyPost(postElement) {
        /* convert “show source” link into javascript function. */
        $(postElement).find(".show-source").each(function() {
                $("a", this).click(function() {
+                       post = getPostElement(this);
+                       rawPostText = $(".post-text.raw-text", post);
+                       rawPostText.toggleClass("hidden");
+                       if (rawPostText.hasClass("hidden")) {
+                               $(".post-text.short-text", post).removeClass("hidden");
+                               $(".post-text.text", post).addClass("hidden");
+                               $(".expand-post-text", post).removeClass("hidden");
+                               $(".shrink-post-text", post).addClass("hidden");
+                       } else {
+                               $(".post-text.short-text", post).addClass("hidden");
+                               $(".post-text.text", post).addClass("hidden");
+                               $(".expand-post-text", post).addClass("hidden");
+                               $(".shrink-post-text", post).addClass("hidden");
+                       }
+                       return false;
+               });
+       });
+
+       /* convert “show more” link into javascript function. */
+       $(postElement).find(".expand-post-text").each(function() {
+               $(this).click(function() {
                        $(".post-text.text", getPostElement(this)).toggleClass("hidden");
-                       $(".post-text.raw-text", getPostElement(this)).toggleClass("hidden");
+                       $(".post-text.short-text", getPostElement(this)).toggleClass("hidden");
+                       $(".expand-post-text", getPostElement(this)).toggleClass("hidden");
+                       $(".shrink-post-text", getPostElement(this)).toggleClass("hidden");
                        return false;
                });
        });
+       $(postElement).find(".shrink-post-text").each(function() {
+               $(this).click(function() {
+                       $(".post-text.text", getPostElement(this)).toggleClass("hidden");
+                       $(".post-text.short-text", getPostElement(this)).toggleClass("hidden");
+                       $(".expand-post-text", getPostElement(this)).toggleClass("hidden");
+                       $(".shrink-post-text", getPostElement(this)).toggleClass("hidden");
+                       return false;
+               })
+       });
+
+       /* ajaxify author/post links */
+       $(".post-status-line .permalink a", postElement).click(function() {
+               if (!$(".create-reply", postElement).hasClass("hidden")) {
+                       textArea = $("input.reply-input", postElement).focus().data("textarea");
+                       $(textArea).replaceSelection($(this).attr("href"));
+               }
+               return false;
+       });
 
        /* add “comment” link. */
-       addCommentLink(getPostId(postElement), getPostAuthor(postElement), postElement, $(postElement).find(".post-status-line .time"));
+       addCommentLink(getPostId(postElement), getPostAuthor(postElement), postElement, $(postElement).find(".post-status-line .permalink-author"));
 
        /* process all replies. */
        replyIds = [];
@@ -794,6 +855,46 @@ function ajaxifyPost(postElement) {
 
        /* hide reply input field. */
        $(postElement).find(".create-reply").addClass("hidden");
+
+       /* show Sone menu when hovering over the avatar. */
+       $(postElement).find(".post-avatar").mouseover(function() {
+               $(".sone-menu:visible").fadeOut();
+               $(".sone-post-menu", postElement).mouseleave(function() {
+                       $(this).fadeOut();
+               }).fadeIn();
+               return false;
+       });
+       (function(postElement) {
+               var soneId = $(".sone-menu-id", postElement).text();
+               $(".sone-post-menu .follow", postElement).click(function() {
+                       var followElement = this;
+                       ajaxGet("followSone.ajax", { "sone": soneId, "formPassword": getFormPassword() }, function() {
+                               $(followElement).addClass("hidden");
+                               $(followElement).parent().find(".unfollow").removeClass("hidden");
+                               $("#sone .sone-menu").each(function() {
+                                       if (getMenuSone(this) == soneId) {
+                                               $(".follow", this).toggleClass("hidden", true);
+                                               $(".unfollow", this).toggleClass("hidden", false);
+                                       }
+                               });
+                       });
+                       return false;
+               });
+               $(".sone-post-menu .unfollow", postElement).click(function() {
+                       var unfollowElement = this;
+                       ajaxGet("unfollowSone.ajax", { "sone": soneId, "formPassword": getFormPassword() }, function() {
+                               $(unfollowElement).addClass("hidden");
+                               $(unfollowElement).parent().find(".follow").removeClass("hidden");
+                               $("#sone .sone-menu").each(function() {
+                                       if (getMenuSone(this) == soneId) {
+                                               $(".follow", this).toggleClass("hidden", false);
+                                               $(".unfollow", this).toggleClass("hidden", true);
+                                       }
+                               });
+                       });
+                       return false;
+               });
+       })(postElement);
 }
 
 /**
@@ -818,13 +919,55 @@ function ajaxifyReply(replyElement) {
                        });
                });
        })(replyElement);
-       addCommentLink(getPostId(replyElement), getReplyAuthor(replyElement), replyElement, $(replyElement).find(".reply-status-line .time"));
+
+       /* ajaxify author links */
+       $(".reply-status-line .permalink a", replyElement).click(function() {
+               if (!$(".create-reply", getPostElement(replyElement)).hasClass("hidden")) {
+                       textArea = $("input.reply-input", getPostElement(replyElement)).focus().data("textarea");
+                       $(textArea).replaceSelection($(this).attr("href"));
+               }
+               return false;
+       });
+
+       addCommentLink(getPostId(replyElement), getReplyAuthor(replyElement), replyElement, $(replyElement).find(".reply-status-line .permalink-author"));
 
        /* convert “show source” link into javascript function. */
        $(replyElement).find(".show-reply-source").each(function() {
                $("a", this).click(function() {
+                       reply = getReplyElement(this);
+                       rawReplyText = $(".reply-text.raw-text", reply);
+                       rawReplyText.toggleClass("hidden");
+                       if (rawReplyText.hasClass("hidden")) {
+                               $(".reply-text.short-text", reply).removeClass("hidden");
+                               $(".reply-text.text", reply).addClass("hidden");
+                               $(".expand-reply-text", reply).removeClass("hidden");
+                               $(".shrink-reply-text", reply).addClass("hidden");
+                       } else {
+                               $(".reply-text.short-text", reply).addClass("hidden");
+                               $(".reply-text.text", reply).addClass("hidden");
+                               $(".expand-reply-text", reply).addClass("hidden");
+                               $(".shrink-reply-text", reply).addClass("hidden");
+                       }
+                       return false;
+               });
+       });
+
+       /* convert “show more” link into javascript function. */
+       $(replyElement).find(".expand-reply-text").each(function() {
+               $(this).click(function() {
+                       $(".reply-text.text", getReplyElement(this)).toggleClass("hidden");
+                       $(".reply-text.short-text", getReplyElement(this)).toggleClass("hidden");
+                       $(".expand-reply-text", getReplyElement(this)).toggleClass("hidden");
+                       $(".shrink-reply-text", getReplyElement(this)).toggleClass("hidden");
+                       return false;
+               });
+       });
+       $(replyElement).find(".shrink-reply-text").each(function() {
+               $(this).click(function() {
                        $(".reply-text.text", getReplyElement(this)).toggleClass("hidden");
-                       $(".reply-text.raw-text", getReplyElement(this)).toggleClass("hidden");
+                       $(".reply-text.short-text", getReplyElement(this)).toggleClass("hidden");
+                       $(".expand-reply-text", getReplyElement(this)).toggleClass("hidden");
+                       $(".shrink-reply-text", getReplyElement(this)).toggleClass("hidden");
                        return false;
                });
        });
@@ -842,6 +985,46 @@ function ajaxifyReply(replyElement) {
                untrustSone(getReplyAuthor(this));
                return false;
        });
+
+       /* show Sone menu when hovering over the avatar. */
+       $(replyElement).find(".reply-avatar").mouseover(function() {
+               $(".sone-menu:visible").fadeOut();
+               $(".sone-reply-menu", replyElement).mouseleave(function() {
+                       $(this).fadeOut();
+               }).fadeIn();
+               return false;
+       });
+       (function(replyElement) {
+               var soneId = $(".sone-menu-id", replyElement).text();
+               $(".sone-menu .follow", replyElement).click(function() {
+                       var followElement = this;
+                       ajaxGet("followSone.ajax", { "sone": soneId, "formPassword": getFormPassword() }, function() {
+                               $(followElement).addClass("hidden");
+                               $(followElement).parent().find(".unfollow").removeClass("hidden");
+                               $("#sone .sone-menu").each(function() {
+                                       if (getMenuSone(this) == soneId) {
+                                               $(".follow", this).toggleClass("hidden", true);
+                                               $(".unfollow", this).toggleClass("hidden", false);
+                                       }
+                               });
+                       });
+                       return false;
+               });
+               $(".sone-menu .unfollow", replyElement).click(function() {
+                       var unfollowElement = this;
+                       ajaxGet("unfollowSone.ajax", { "sone": soneId, "formPassword": getFormPassword() }, function() {
+                               $(unfollowElement).addClass("hidden");
+                               $(unfollowElement).parent().find(".follow").removeClass("hidden");
+                               $("#sone .sone-menu").each(function() {
+                                       if (getMenuSone(this) == soneId) {
+                                               $(".follow", this).toggleClass("hidden", false);
+                                               $(".unfollow", this).toggleClass("hidden", true);
+                                       }
+                               });
+                       });
+                       return false;
+               });
+       })(replyElement);
 }
 
 /**
@@ -860,19 +1043,23 @@ function ajaxifyNotification(notification) {
                notification.find(".text").addClass("hidden");
        }
        notification.find("form.mark-as-read button").click(function() {
-               $.getJSON("markAsKnown.ajax", {"formPassword": getFormPassword(), "type": $(":input[name=type]", this.form).val(), "id": $(":input[name=id]", this.form).val()});
+               allIds = $(":input[name=id]", this.form).val().split(" ");
+               for (index = 0; index < allIds.length; index += 16) {
+                       ids = allIds.slice(index, index + 16).join(" ");
+                       ajaxGet("markAsKnown.ajax", {"formPassword": getFormPassword(), "type": $(":input[name=type]", this.form).val(), "id": ids});
+               }
        });
        notification.find("a[class^='link-']").each(function() {
                linkElement = $(this);
                if (linkElement.is("[href^='viewPost']")) {
                        id = linkElement.attr("class").substr(5);
                        if (hasPost(id)) {
-                               linkElement.attr("href", "#post-" + id);
+                               linkElement.attr("href", "#post-" + id).addClass("in-page-link");
                        }
                }
        });
        notification.find("form.dismiss button").click(function() {
-               $.getJSON("dismissNotification.ajax", { "formPassword" : getFormPassword(), "notification" : notification.attr("id") }, function(data, textStatus) {
+               ajaxGet("dismissNotification.ajax", { "formPassword" : getFormPassword(), "notification" : notification.attr("id") }, function(data, textStatus) {
                        /* dismiss in case of error, too. */
                        notification.slideUp();
                }, function(xmlHttpRequest, textStatus, error) {
@@ -912,8 +1099,8 @@ function checkForRemovedSones(oldNotification, newNotification) {
        if (getNotificationId(oldNotification) != "new-sone-notification") {
                return;
        }
-       oldIds = getElementIds(oldNotification, ".sone-id");
-       newIds = getElementIds(newNotification, ".sone-id");
+       oldIds = getElementIds(oldNotification, ".new-sone-id");
+       newIds = getElementIds(newNotification, ".new-sone-id");
        $.each(oldIds, function(index, value) {
                if ($.inArray(value, newIds) == -1) {
                        markSoneAsKnown(getSone(value), true);
@@ -954,7 +1141,7 @@ function checkForRemovedPosts(oldNotification, newNotification) {
  *            The new notification element
  */
 function checkForRemovedReplies(oldNotification, newNotification) {
-       if (getNotificationId(oldNotification) != "new-replies-notification") {
+       if (getNotificationId(oldNotification) != "new-reply-notification") {
                return;
        }
        oldIds = getElementIds(oldNotification, ".reply-id");
@@ -967,12 +1154,16 @@ function checkForRemovedReplies(oldNotification, newNotification) {
 }
 
 function getStatus() {
-       $.getJSON("getStatus.ajax", {"loadAllSones": isKnownSonesPage()}, function(data, textStatus) {
+       ajaxGet("getStatus.ajax", isViewSonePage() ? {"soneIds": getShownSoneId() } : {"loadAllSones": isKnownSonesPage()}, function(data, textStatus) {
                if ((data != null) && data.success) {
                        /* process Sone information. */
                        $.each(data.sones, function(index, value) {
-                               updateSoneStatus(value.id, value.name, value.status, value.modified, value.locked, value.lastUpdatedUnknown ? null : value.lastUpdated);
+                               updateSoneStatus(value.id, value.name, value.status, value.modified, value.locked, value.lastUpdatedUnknown ? null : value.lastUpdated, value.lastUpdatedText);
                        });
+                       notLoggedIn = !data.loggedIn;
+                       if (!notLoggedIn) {
+                               showOfflineMarker(!online);
+                       }
                        /* search for removed notifications. */
                        $("#sone #notification-area .notification").each(function() {
                                notificationId = $(this).attr("id");
@@ -985,7 +1176,7 @@ function getStatus() {
                                });
                                if (!foundNotification) {
                                        if (notificationId == "new-sone-notification") {
-                                               $(".sone-id", this).each(function(index, element) {
+                                               $(".new-sone-id", this).each(function(index, element) {
                                                        soneId = $(this).text();
                                                        markSoneAsKnown(getSone(soneId), true);
                                                });
@@ -994,7 +1185,7 @@ function getStatus() {
                                                        postId = $(this).text();
                                                        markPostAsKnown(getPost(postId), true);
                                                });
-                                       } else if (notificationId == "new-replies-notification") {
+                                       } else if (notificationId == "new-reply-notification") {
                                                $(".reply-id", this).each(function(index, element) {
                                                        replyId = $(this).text();
                                                        markReplyAsKnown(getReply(replyId), true);
@@ -1010,25 +1201,16 @@ function getStatus() {
                                }
                        });
                        /* process notifications. */
+                       notificationIds = [];
                        $.each(data.notifications, function(index, value) {
                                oldNotification = getNotification(value.id);
-                               notification = ajaxifyNotification(createNotification(value.id, value.text, value.dismissable)).hide();
-                               if (oldNotification.length != 0) {
-                                       if ((oldNotification.find(".short-text").length > 0) && (notification.find(".short-text").length > 0)) {
-                                               opened = oldNotification.is(":visible") && oldNotification.find(".short-text").hasClass("hidden");
-                                               notification.find(".short-text").toggleClass("hidden", opened);
-                                               notification.find(".text").toggleClass("hidden", !opened);
-                                       }
-                                       checkForRemovedSones(oldNotification, notification);
-                                       checkForRemovedPosts(oldNotification, notification);
-                                       checkForRemovedReplies(oldNotification, notification);
-                                       oldNotification.replaceWith(notification.show());
-                               } else {
-                                       $("#sone #notification-area").append(notification);
-                                       notification.slideDown();
-                                       setActivity();
+                               if ((oldNotification.length == 0) || (value.lastUpdatedTime > getNotificationLastUpdatedTime(oldNotification))) {
+                                       notificationIds.push(value.id);
                                }
                        });
+                       if (notificationIds.length > 0) {
+                               loadNotifications(notificationIds);
+                       }
                        /* process new posts. */
                        $.each(data.newPosts, function(index, value) {
                                loadNewPost(value.id, value.sone, value.recipient, value.time);
@@ -1043,10 +1225,46 @@ function getStatus() {
                        /* data.success was false, wait 30 seconds. */
                        setTimeout(getStatus, 30000);
                }
-       }, function(xmlHttpRequest, textStatus, error) {
-               /* something really bad happend, wait a minute. */
-               setTimeout(getStatus, 60000);
-       })
+       }, function() {
+               statusRequestQueued = false;
+               ajaxError();
+       });
+}
+
+/**
+ * Requests multiple notifications from Sone and displays them.
+ *
+ * @param notificationIds
+ *            Array of IDs of the notifications to load
+ */
+function loadNotifications(notificationIds) {
+       ajaxGet("getNotification.ajax", {"notifications": notificationIds.join(",")}, function(data, textStatus) {
+               if (!data || !data.success) {
+                       // TODO - show error
+                       return;
+               }
+               $.each(data.notifications, function(index, value) {
+                       oldNotification = getNotification(value.id);
+                       notification = ajaxifyNotification(createNotification(value.id, value.lastUpdatedTime, value.text, value.dismissable)).hide();
+                       if (oldNotification.length != 0) {
+                               if ((oldNotification.find(".short-text").length > 0) && (notification.find(".short-text").length > 0)) {
+                                       opened = oldNotification.is(":visible") && oldNotification.find(".short-text").hasClass("hidden");
+                                       notification.find(".short-text").toggleClass("hidden", opened);
+                                       notification.find(".text").toggleClass("hidden", !opened);
+                               }
+                               checkForRemovedSones(oldNotification, notification);
+                               checkForRemovedPosts(oldNotification, notification);
+                               checkForRemovedReplies(oldNotification, notification);
+                               oldNotification.replaceWith(notification.show());
+                       } else {
+                               $("#sone #notification-area").append(notification);
+                               if (value.id.substring(0, 5) != "local") {
+                                       notification.slideDown();
+                                       setActivity();
+                               }
+                       }
+               });
+       });
 }
 
 /**
@@ -1079,6 +1297,22 @@ function isIndexPage() {
 }
 
 /**
+ * Returns the current page of the selected pagination. If no pagination can be
+ * found with the given selector, {@code 1} is returned.
+ *
+ * @param paginationSelector
+ *            The pagination selector
+ * @returns The current page of the pagination
+ */
+function getPage(paginationSelector) {
+       pagination = $(paginationSelector);
+       if (pagination.length > 0) {
+               return $(".current-page", paginationSelector).text();
+       }
+       return 1;
+}
+
+/**
  * Returns whether the current page is a “view Sone” page.
  *
  * @returns {Boolean} <code>true</code> if the current page is a “view Sone”
@@ -1156,9 +1390,9 @@ function loadNewPost(postId, soneId, recipientId, time) {
        if (hasPost(postId)) {
                return;
        }
-       if (!isIndexPage()) {
+       if (!isIndexPage() || (getPage(".pagination-index") > 1)) {
                if (!isViewPostPage() || (getShownPostId() != postId)) {
-                       if (!isViewSonePage() || ((getShownSoneId() != soneId) && (getShownSoneId() != recipientId))) {
+                       if (!isViewSonePage() || ((getShownSoneId() != soneId) && (getShownSoneId() != recipientId)) || (getPage(".post-navigation") > 1)) {
                                return;
                        }
                }
@@ -1166,12 +1400,12 @@ function loadNewPost(postId, soneId, recipientId, time) {
        if (getPostTime($("#sone .post").last()) > time) {
                return;
        }
-       $.getJSON("getPost.ajax", { "post" : postId }, function(data, textStatus) {
+       ajaxGet("getPost.ajax", { "post" : postId }, function(data, textStatus) {
                if ((data != null) && data.success) {
                        if (hasPost(data.post.id)) {
                                return;
                        }
-                       if (!isIndexPage() && !(isViewSonePage() && ((getShownSoneId() == data.post.sone) || (getShownSoneId() == data.post.recipient)))) {
+                       if ((!isIndexPage() || (getPage(".pagination-index") > 1)) && !(isViewSonePage() && ((getShownSoneId() == data.post.sone) || (getShownSoneId() == data.post.recipient) || (getPage(".post-navigation") > 1)))) {
                                return;
                        }
                        var firstOlderPost = null;
@@ -1182,6 +1416,9 @@ function loadNewPost(postId, soneId, recipientId, time) {
                                }
                        });
                        newPost = $(data.post.html).addClass("hidden");
+                       if ($(".post-author-local", newPost).text() == "true") {
+                               newPost.removeClass("new");
+                       }
                        if (firstOlderPost != null) {
                                newPost.insertBefore(firstOlderPost);
                        }
@@ -1200,7 +1437,7 @@ function loadNewReply(replyId, soneId, postId, postSoneId) {
        if (!hasPost(postId)) {
                return;
        }
-       $.getJSON("getReply.ajax", { "reply": replyId }, function(data, textStatus) {
+       ajaxGet("getReply.ajax", { "reply": replyId }, function(data, textStatus) {
                /* find post. */
                if ((data != null) && data.success) {
                        if (hasReply(data.reply.id)) {
@@ -1215,6 +1452,14 @@ function loadNewReply(replyId, soneId, postId, postSoneId) {
                                        }
                                });
                                newReply = $(data.reply.html).addClass("hidden");
+                               if ($(".reply-author-local", newReply).text() == "true") {
+                                       newReply.removeClass("new");
+                                       (function(newReply) {
+                                               setTimeout(function() {
+                                                       markReplyAsKnown(newReply, false);
+                                               }, 5000);
+                                       })(newReply);
+                               }
                                if (firstNewerReply != null) {
                                        newReply.insertBefore(firstNewerReply);
                                } else {
@@ -1244,11 +1489,10 @@ function loadNewReply(replyId, soneId, postId, postSoneId) {
  *            request
  */
 function markSoneAsKnown(soneElement, skipRequest) {
-       if ($(".new", soneElement).length > 0) {
-               if ((typeof skipRequest != "undefined") && !skipRequest) {
-                       $.getJSON("maskAsKnown.ajax", {"formPassword": getFormPassword(), "type": "sone", "id": getSoneId(soneElement)}, function(data, textStatus) {
-                               $(soneElement).removeClass("new");
-                       });
+       if ($(soneElement).is(".new")) {
+               $(soneElement).removeClass("new");
+               if ((typeof skipRequest == "undefined") || !skipRequest) {
+                       ajaxGet("markAsKnown.ajax", {"formPassword": getFormPassword(), "type": "sone", "id": getSoneId(soneElement)});
                }
        }
 }
@@ -1256,15 +1500,15 @@ function markSoneAsKnown(soneElement, skipRequest) {
 function markPostAsKnown(postElements, skipRequest) {
        $(postElements).each(function() {
                postElement = this;
-               if ($(postElement).hasClass("new")) {
+               if ($(postElement).hasClass("new") || ((typeof skipRequest != "undefined"))) {
                        (function(postElement) {
                                $(postElement).removeClass("new");
-                               $(".click-to-show", postElement).removeClass("new");
                                if ((typeof skipRequest == "undefined") || !skipRequest) {
-                                       $.getJSON("markAsKnown.ajax", {"formPassword": getFormPassword(), "type": "post", "id": getPostId(postElement)});
+                                       ajaxGet("markAsKnown.ajax", {"formPassword": getFormPassword(), "type": "post", "id": getPostId(postElement)});
                                }
                        })(postElement);
                }
+               $(".click-to-show", postElement).removeClass("new");
        });
        markReplyAsKnown($(postElements).find(".reply"));
 }
@@ -1272,11 +1516,11 @@ function markPostAsKnown(postElements, skipRequest) {
 function markReplyAsKnown(replyElements, skipRequest) {
        $(replyElements).each(function() {
                replyElement = this;
-               if ($(replyElement).hasClass("new")) {
+               if ($(replyElement).hasClass("new") || ((typeof skipRequest != "undefined"))) {
                        (function(replyElement) {
                                $(replyElement).removeClass("new");
                                if ((typeof skipRequest == "undefined") || !skipRequest) {
-                                       $.getJSON("markAsKnown.ajax", {"formPassword": getFormPassword(), "type": "reply", "id": getReplyId(replyElement)});
+                                       ajaxGet("markAsKnown.ajax", {"formPassword": getFormPassword(), "type": "reply", "id": getReplyId(replyElement)});
                                }
                        })(replyElement);
                }
@@ -1314,7 +1558,7 @@ function updatePostTime(postId, timeText, refreshTime, tooltip) {
  *            Comma-separated post IDs
  */
 function updatePostTimes(postIds) {
-       $.getJSON("getTimes.ajax", { "posts" : postIds }, function(data, textStatus) {
+       ajaxGet("getTimes.ajax", { "posts" : postIds }, function(data, textStatus) {
                if ((data != null) && data.success) {
                        $.each(data.postTimes, function(index, value) {
                                updatePostTime(index, value.timeText, value.refreshTime, value.tooltip);
@@ -1351,7 +1595,7 @@ function updateReplyTime(replyId, timeText, refreshTime, tooltip) {
  *            Comma-separated post IDs
  */
 function updateReplyTimes(replyIds) {
-       $.getJSON("getTimes.ajax", { "replies" : replyIds }, function(data, textStatus) {
+       ajaxGet("getTimes.ajax", { "replies" : replyIds }, function(data, textStatus) {
                if ((data != null) && data.success) {
                        $.each(data.replyTimes, function(index, value) {
                                updateReplyTime(index, value.timeText, value.refreshTime, value.tooltip);
@@ -1440,10 +1684,10 @@ function changeIcon(iconUrl) {
  *            <code>true</code> if the notification can be dismissed by the
  *            user
  */
-function createNotification(id, text, dismissable) {
-       notification = $("<div></div>").addClass("notification").attr("id", id);
+function createNotification(id, lastUpdatedTime, text, dismissable) {
+       notification = $("<div></div>").addClass("notification").attr("id", id).attr("lastUpdatedTime", lastUpdatedTime);
        if (dismissable) {
-               dismissForm = $("#sone #notification-area #notification-dismiss-template").clone().removeClass("hidden").removeAttr("id")
+               dismissForm = $("#sone #notification-area #notification-dismiss-template").clone().removeClass("hidden").removeAttr("id");
                dismissForm.find("input[name=notification]").val(id);
                notification.append(dismissForm);
        }
@@ -1469,7 +1713,7 @@ function showNotificationDetails(notificationId) {
  *            The ID of the field to delete
  */
 function deleteProfileField(fieldId) {
-       $.getJSON("deleteProfileField.ajax", {"formPassword": getFormPassword(), "field": fieldId}, function(data, textStatus) {
+       ajaxGet("deleteProfileField.ajax", {"formPassword": getFormPassword(), "field": fieldId}, function(data, textStatus) {
                if (data && data.success) {
                        $("#sone .profile-field#" + data.field.id).slideUp();
                }
@@ -1487,7 +1731,7 @@ function deleteProfileField(fieldId) {
  *            Called when the renaming was successful
  */
 function editProfileField(fieldId, newName, successFunction) {
-       $.getJSON("editProfileField.ajax", {"formPassword": getFormPassword(), "field": fieldId, "name": newName}, function(data, textStatus) {
+       ajaxGet("editProfileField.ajax", {"formPassword": getFormPassword(), "field": fieldId, "name": newName}, function(data, textStatus) {
                if (data && data.success) {
                        successFunction();
                }
@@ -1505,7 +1749,7 @@ function editProfileField(fieldId, newName, successFunction) {
  *            Function to call on success
  */
 function moveProfileField(fieldId, direction, successFunction) {
-       $.getJSON("moveProfileField.ajax", {"formPassword": getFormPassword(), "field": fieldId, "direction": direction}, function(data, textStatus) {
+       ajaxGet("moveProfileField.ajax", {"formPassword": getFormPassword(), "field": fieldId, "direction": direction}, function(data, textStatus) {
                if (data && data.success) {
                        successFunction();
                }
@@ -1536,11 +1780,53 @@ function moveProfileFieldDown(fieldId, successFunction) {
        moveProfileField(fieldId, "down", successFunction);
 }
 
+var statusRequestQueued = true;
+
+/**
+ * Sets the status of the web interface as offline.
+ */
+function ajaxError() {
+       online = false;
+       showOfflineMarker(true);
+       if (!statusRequestQueued) {
+               setTimeout(getStatus, 5000);
+               statusRequestQueued = true;
+       }
+}
+
+/**
+ * Sets the status of the web interface as online.
+ */
+function ajaxSuccess() {
+       online = true;
+       showOfflineMarker(!online || (initiallyLoggedIn && notLoggedIn));
+}
+
+/**
+ * Shows or hides the offline marker.
+ *
+ * @param visible
+ *            {@code true} to display the offline marker, {@code false} to hide
+ *            it
+ */
+function showOfflineMarker(visible) {
+       /* jQuery documentation says toggle() works the other way around?! */
+       $("#sone #offline-marker").toggle(visible);
+       if (visible) {
+               $("#sone #main").addClass("offline");
+       } else {
+               $("#sone #main").removeClass("offline");
+       }
+}
+
 //
 // EVERYTHING BELOW HERE IS EXECUTED AFTER LOADING THE PAGE
 //
 
 var focus = true;
+var online = true;
+var initiallyLoggedIn = $("#sone #loggedIn").text() == "true";
+var notLoggedIn = !initiallyLoggedIn;
 
 $(document).ready(function() {
 
@@ -1562,7 +1848,7 @@ $(document).ready(function() {
                        }
                        sender = $(this).find(":input[name=sender]").val();
                        text = $(this).find(":input[name=text]:enabled").val();
-                       $.getJSON("createPost.ajax", { "formPassword": getFormPassword(), "sender": sender, "text": text }, function(data, textStatus) {
+                       ajaxGet("createPost.ajax", { "formPassword": getFormPassword(), "sender": sender, "text": text }, function(data, textStatus) {
                                button.removeAttr("disabled");
                        });
                        $(this).find(":input[name=sender]").val(getCurrentSoneId());
@@ -1591,7 +1877,7 @@ $(document).ready(function() {
                $("#sone #post-message").submit(function() {
                        sender = $(this).find(":input[name=sender]").val();
                        text = $(this).find(":input[name=text]:enabled").val();
-                       $.getJSON("createPost.ajax", { "formPassword": getFormPassword(), "recipient": getShownSoneId(), "sender": sender, "text": text });
+                       ajaxGet("createPost.ajax", { "formPassword": getFormPassword(), "recipient": getShownSoneId(), "sender": sender, "text": text });
                        $(this).find(":input[name=sender]").val(getCurrentSoneId());
                        $(this).find(":input[name=text]:enabled").val("").blur();
                        $(this).find(".sender").hide();
@@ -1626,7 +1912,7 @@ $(document).ready(function() {
                                allReplies = $(this).find(".reply");
                                if (allReplies.length > 2) {
                                        newHidden = false;
-                                       for (replyIndex = 0; replyIndex < (allReplies.length - 2); ++replyIndex) {
+                                       for (replyIndex = 0; !newHidden && (replyIndex < (allReplies.length - 2)); ++replyIndex) {
                                                $(allReplies[replyIndex]).addClass("hidden");
                                                newHidden |= $(allReplies[replyIndex]).hasClass("new");
                                        }
@@ -1665,6 +1951,6 @@ $(document).ready(function() {
                resetActivity();
        }).blur(function() {
                focus = false;
-       })
+       });
 
 });
index fe9cf34..cd9eddf 100644 (file)
@@ -1,12 +1,16 @@
-<div id="sone" class="<%ifnull ! currentSone>online<%else>offline<%/if>">
+<div id="sone">
 
        <div id="formPassword"><% formPassword|html></div>
        <div id="currentSoneId" class="hidden"><% currentSone.id|html></div>
+       <div id="loggedIn" class="hidden"><%ifnull !currentSone>true<%else>false<%/if></div>
 
        <script src="javascript/jquery-1.4.2.js" language="javascript"></script>
        <script src="javascript/jquery.url.js" language="javascript"></script>
+       <script src="javascript/jquery.fieldselection.js" language="javascript"></script>
        <script src="javascript/sone.js" language="javascript"></script>
 
+       <div id="offline-marker"></div>
+
        <div id="main">
 
                <div id="notification-area">
@@ -18,8 +22,8 @@
                                <button type="submit"><%= Notification.Button.Dismiss|l10n|html></button>
                        </form>
 
-                       <%foreach webInterface.notifications.all notification>
-                               <div class="notification" id="<% notification.id|html>">
+                       <%foreach notifications notification>
+                               <div class="notification" id="<% notification.id|html>" lastUpdatedTime="<%notification.lastUpdatedTime|html>">
                                        <%if notification.dismissable>
                                                <form class="dismiss" action="dismissNotification.html" method="post">
                                                        <input type="hidden" name="formPassword" value="<% formPassword|html>" />
index a5efc97..f8e6f11 100644 (file)
@@ -1,5 +1,5 @@
 <%if pagination.necessary>
-       <div class="navigation">
+       <div class="navigation <%paginationName|html>">
                <div class="first"><%if ! pagination.first><a href="<% request|change nameKey=pageParameter value=0>">«</a><%else><span>«</span><%/if></div>
                <div class="previous"><%if ! pagination.first><a href="<% request|change nameKey=pageParameter key=pagination.previousPage>">‹</a><%else><span>‹</span><%/if></div>
                <div class="current-page"><% pagination.pageNumber></div>
diff --git a/src/main/resources/templates/include/soneMenu.html b/src/main/resources/templates/include/soneMenu.html
new file mode 100644 (file)
index 0000000..e755662
--- /dev/null
@@ -0,0 +1,17 @@
+<div class="sone-menu <% class|css|html>">
+       <div class="sone-menu-id hidden"><%sone.id|html></div>
+       <img class="avatar" src="/WebOfTrust/GetIdenticon?identity=<%sone.id|html>&amp;width=64&amp;height=64" width="64" height="64" alt="Avatar Image" />
+       <div class="inner-menu">
+               <div>
+                       <a class="author" href="viewSone.html?sone=<%sone.id|html>"><%sone.niceName|html></a>
+                       <span class="author-wot-link">(<a href="/WebOfTrust/ShowIdentity?id=<%sone.id|html>"><% =View.Post.WebOfTrustLink|l10n|html></a>)</span>
+               </div>
+               <div><%= View.Sone.Stats.Posts|l10n 0=sone.posts.size>, <%= View.Sone.Stats.Replies|l10n 0=sone.replies.size></div>
+               <%if !sone.local>
+                       <div>
+                               <a class="follow<%if sone.friend> hidden<%/if>"><%= View.Sone.Button.FollowSone|l10n|html></a>
+                               <a class="unfollow<%if !sone.friend> hidden<%/if>"><%= View.Sone.Button.UnfollowSone|l10n|html></a>
+                       </div>
+               <%/if>
+       </div>
+</div>
index 43d4d70..881aca4 100644 (file)
@@ -4,7 +4,7 @@
        <label for="sender"><%= Page.Index.Label.Sender|l10n|html></label>
        <div class="sender">
                <select name="sender" title="<%= View.UpdateStatus.Text.ChooseSenderIdentity|l10n|html>">
-                       <%foreach localSones localSone>
+                       <%foreach localSones localSone|sort>
                                <option value="<% localSone.id|html>"<%if localSone.current> selected="selected"<%/if>><% localSone.niceName|html></option>
                        <%/foreach>
                </select>
index 4be1bcc..89a8ba5 100644 (file)
@@ -1,8 +1,10 @@
-<div id="<% post.id|html>" class="post<%if loop.last> last<%/if><%if post.new> new<%/if>">
+<div id="<% post.id|html>" class="post<%if loop.last> last<%/if><%if !post.sone.local><%if post.new> new<%/if><%/if>">
        <a name="post-<% post.id|html>"></a>
        <div class="post-time hidden"><% post.time|html></div>
        <div class="post-author hidden"><% post.sone.id|html></div>
-       <div class="avatar">
+       <div class="post-author-local hidden"><% post.sone.local></div>
+       <%include include/soneMenu.html class=="sone-post-menu" sone=post.sone>
+       <div class="avatar post-avatar" >
                <%if post.loaded>
                        <img src="/WebOfTrust/GetIdenticon?identity=<% post.sone.id|html>&amp;width=48&height=48" width="48" height="48" alt="Avatar Image" />
                <%else>
                        <%/if>
                        <% post.text|html|store key=originalText text=true>
                        <% post.text|parse sone=post.sone|store key=parsedText text=true>
+                       <% post.text|parse sone=post.sone length=core.preferences.charactersPerPost|store key=shortText text=true>
                        <div class="post-text raw-text<%if !raw> hidden<%/if>"><% originalText></div>
-                       <div class="post-text text<%if raw> hidden<%/if>"><% parsedText></div>
+                       <div class="post-text text<%if raw> hidden<%/if><%if !shortText|match key=parsedText> hidden<%/if>"><% parsedText></div>
+                       <div class="post-text short-text<%if raw> hidden<%/if><%if shortText|match key=parsedText> hidden<%/if>"><% shortText></div>
+                       <%if !shortText|match key=parsedText><%if !raw><a class="expand-post-text" href="viewPost.html?post=<% post.id|html>&amp;raw=true"><%= View.Post.ShowMore|l10n|html></a><%/if><%/if>
+                       <%if !shortText|match key=parsedText><%if !raw><a class="shrink-post-text hidden"><%= View.Post.ShowLess|l10n|html></a><%/if><%/if>
                </div>
                <div class="post-status-line status-line<%if !post.loaded> hidden<%/if>">
                        <div class="bookmarks">
                        </div>
                        <span class='separator'>·</span>
                        <div class="time"><a href="viewPost.html?post=<% post.id|html>"><% post.time|date format="MMM d, yyyy, HH:mm:ss"></a></div>
+                       <span class='separator'>·</span>
+                       <div class="permalink permalink-post"><a href="post://<%post.id|html>">[<%= View.Post.Permalink|l10n|html>]</a></div>
+                       <span class='separator'>·</span>
+                       <div class="permalink permalink-author"><a href="sone://<%post.sone.id|html>">[<%= View.Post.PermalinkAuthor|l10n|html>]</a></div>
                        <%if ! originalText|match key=parsedText>
                                <span class='separator'>·</span>
                                <div class="show-source"><a href="viewPost.html?post=<% post.id|html>&amp;raw=<%if raw>false<%else>true<%/if>"><%= View.Post.ShowSource|l10n|html></a></div>
index 5dc26fb..ee7299e 100644 (file)
@@ -1,8 +1,10 @@
-<div id="<% reply.id|html>" class="reply<%if reply.new> new<%/if>">
+<div id="<% reply.id|html>" class="reply<%if !reply.sone.local><%if reply.new> new<%/if><%/if>">
        <a name="reply-<% reply.id|html>"></a>
        <div class="reply-time hidden"><% reply.time|html></div>
        <div class="reply-author hidden"><% reply.sone.id|html></div>
-       <div class="avatar">
+       <div class="reply-author-local hidden"><% reply.sone.local></div>
+       <%include include/soneMenu.html class=="sone-reply-menu" sone=reply.sone>
+       <div class="avatar reply-avatar">
                <img src="/WebOfTrust/GetIdenticon?identity=<% reply.sone.id|html>&amp;width=36&height=36" width="36" height="36" alt="Avatar Image" />
        </div>
        <div class="inner-part">
                        <div class="author profile-link"><a href="viewSone.html?sone=<% reply.sone.id|html>"><% reply.sone.niceName|html></a></div>
                        <% reply.text|html|store key=originalText text=true>
                        <% reply.text|parse sone=reply.sone|store key=parsedText text=true>
+                       <% reply.text|parse sone=reply.sone length=core.preferences.charactersPerPost|store key=shortText text=true>
                        <div class="reply-text raw-text<%if !raw> hidden<%/if>"><% originalText></div>
-                       <div class="reply-text text<%if raw> hidden<%/if>"><% parsedText></div>
+                       <div class="reply-text text<%if raw> hidden<%/if><%if !shortText|match key=parsedText> hidden<%/if>"><% parsedText></div>
+                       <div class="reply-text short-text<%if raw> hidden<%/if><%if shortText|match key=parsedText> hidden<%/if>"><% shortText></div>
+                       <%if !shortText|match key=parsedText><%if !raw><a class="expand-reply-text" href="viewPost.html?post=<% reply.post.id|html>&amp;raw=true"><%= View.Post.ShowMore|l10n|html></a><%/if><%/if>
+                       <%if !shortText|match key=parsedText><%if !raw><a class="shrink-reply-text hidden"><%= View.Post.ShowLess|l10n|html></a><%/if><%/if>
                </div>
                <div class="reply-status-line status-line">
                        <div class="time"><% reply.time|date format="MMM d, yyyy, HH:mm:ss"></div>
+                       <span class='separator'>·</span>
+                       <div class="permalink permalink-author"><a href="sone://<%reply.sone.id|html>">[<%= View.Post.PermalinkAuthor|l10n|html>]</a></div>
                        <%if ! originalText|match key=parsedText>
                                <span class='separator'>·</span>
                                <div class="show-reply-source"><a href="viewPost.html?post=<% post.id|html>&amp;raw=<%if raw>false<%else>true<%/if>"><%= View.Post.ShowSource|l10n|html></a></div>
index c403b0d..ba5c8e4 100644 (file)
@@ -5,8 +5,11 @@
        <div class="download-marker" title="<%= View.Sone.Status.Downloading|l10n|html>">⬊</div>
        <div class="insert-marker" title="<%= View.Sone.Status.Inserting|l10n|html>">⬈</div>
        <div class="idle-marker" title="<%= View.Sone.Status.Idle|l10n|html>">✔</div>
-       <div class="last-update"><%= View.Sone.Label.LastUpdate|l10n|html> <span class="time"><% sone.time|unknown|date format="MMM d, yyyy, HH:mm:ss"></span></div>
-       <div class="profile-link"><a href="viewSone.html?sone=<% sone.id|html>" title="<% sone.requestUri|html>"><% sone.niceName|html></a></div>
+       <div class="last-update"><%= View.Sone.Label.LastUpdate|l10n|html> <span class="time" title="<% sone.time|unknown|date format="MMM d, yyyy, HH:mm:ss">"><%sone.lastUpdatedText|html></span></div>
+       <div>
+               <div class="profile-link"><a href="viewSone.html?sone=<% sone.id|html>" title="<% sone.requestUri|html>"><% sone.niceName|html></a></div>
+               <div class="sone-stats">(<%= View.Sone.Stats.Posts|l10n 0=sone.posts.size>, <%= View.Sone.Stats.Replies|l10n 0=sone.replies.size>)</div>
+       </div>
        <div class="short-request-uri"><% sone.requestUri|substring start=4 length=43|html></div>
        <div class="hidden"><% sone.blacklisted></div>
        <%if sone.local>
index 59bfee7..f79e52d 100644 (file)
@@ -8,7 +8,7 @@
 
        <div id="posts">
                <%= page|store key=pageParameter>
-               <%include include/pagination.html>
+               <%include include/pagination.html paginationName==pagination-index>
                <%foreach pagination.items post>
                        <%include include/viewPost.html>
                <%foreachelse>
index 73e35a8..7295361 100644 (file)
@@ -6,7 +6,7 @@
                <h1>Sone “<% currentSone.name|html>”</h1>
                <p>
                        This page should not be viewed directly; it is merely a reminder
-                       that you should install the <i>Sone</i> plugin.
+                       that you should install the <a href="/USK@nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI,DuQSUZiI~agF8c-6tjsFFGuZ8eICrzWCILB60nT8KKo,AQACAAE/sone/<% currentEdition>/">Sone</a> plugin.
                </p>
        </body>
 </html>
index 33f0e27..260a321 100644 (file)
@@ -1,8 +1,74 @@
 <%include include/head.html>
 
        <div class="page-id hidden">known-sones</div>
+       
+       <script language="javascript">
+
+               $(document).ready(function() {
+                       $("select[name=sort]").change(function() {
+                               value = $(this).val();
+                               if ((value == "activity") || (value == "posts")) {
+                                       $("select[name=order]").val("desc");
+                               } else if (value == "name") {
+                                       $("select[name=order]").val("asc");
+                               } 
+                       });
+                       $("#sort-options select").change(function() {
+                               this.form.submit();
+                       });
+               });
+       
+       </script>
 
        <h1><%= Page.KnownSones.Page.Title|l10n|html></h1>
+       
+       <div id="sort-options">
+               <form action="knownSones.html" method="get">
+                       <div>
+                               Sort:
+                               <select name="sort">
+                                       <option value="name"<%if sort|match value="name"> selected="selected"<%/if>>Name</option>
+                                       <option value="activity"<%if sort|match value="activity"> selected="selected"<%/if>>Last activity</option>
+                                       <option value="posts"<%if sort|match value="posts"> selected="selected"<%/if>>Number of posts</option>
+                               </select>
+                               <select name="order">
+                                       <option value="asc"<%if order|match value="asc"> selected="selected"<%/if>>Ascending</option>
+                                       <option value="desc"<%if order|match value="desc"> selected="selected"<%/if>>Descending</option>
+                               </select>
+                       </div>
+                       <%ifnull !currentSone>
+                               <div>
+                                       Followed Sones:
+                                       <select name="followedSones">
+                                               <option value="none"></option>
+                                               <option value="show-only"<%if followedSones|match value="show-only"> selected="selected"<%/if>>Show only followed Sones</option>
+                                               <option value="hide"<%if followedSones|match value="hide"> selected="selected"<%/if>>Hide followed Sones</option>
+                                       </select>
+                               </div>
+                       <%/if>
+                       <div>
+                               <button type="submit">Apply</button>
+                       </div>
+               </form>
+       </div>
+
+       <div>
+               <form action="followSone.html" method="post">
+                       <input type="hidden" name="formPassword" value="<%formPassword|html>" />
+                       <input type="hidden" name="returnPage" value="<%request.uri|html>" />
+                       <input type="hidden" name="sone" value="<%foreach pagination.items sone><%if !sone.friend><%if !sone.current><%sone.id> <%/if><%/if><%/foreach>" />
+                       <button type="submit"><%= Page.KnownSones.Button.FollowAllSones|l10n|html></button>
+               </form>
+       </div>
+
+       <div>
+               <form action="unfollowSone.html" method="post">
+                       <input type="hidden" name="formPassword" value="<%formPassword|html>" />
+                       <input type="hidden" name="returnPage" value="<%request.uri|html>" />
+                       <input type="hidden" name="sone" value="<%foreach pagination.items sone><%if sone.friend><%sone.id> <%/if><%/foreach>" />
+                       <button type="submit"><%= Page.KnownSones.Button.UnfollowAllSones|l10n|html></button>
+               </form>
+       </div>
 
        <div id="known-sones">
                <%= page|store key=pageParameter>
diff --git a/src/main/resources/templates/notify/mentionNotification.html b/src/main/resources/templates/notify/mentionNotification.html
new file mode 100644 (file)
index 0000000..10e9ad3
--- /dev/null
@@ -0,0 +1,11 @@
+<div class="short-text hidden">
+       <%= Notification.Mention.ShortText|l10n|html>
+       <a class="link" onclick="showNotificationDetails('<%notification.id|html>'); return false;"><%= Notification.ClickHereToRead|l10n|html></a>
+</div>
+<div class="text">
+       <%= Notification.Mention.Text|l10n|html>
+       <%foreach posts post|unique>
+               <div class="hidden post-id"><%post.id|html></div>
+               <a class="link-<% post.id|html>" href="viewPost.html?post=<% post.id|html>"><% post.sone.niceName|html></a><%notlast>,<%/notlast><%last>.<%/last>
+       <%/foreach>
+</div>
index 54008dc..0cd71ff 100644 (file)
@@ -1,15 +1,15 @@
+<form class="mark-as-read" action="markAsKnown.html" method="post">
+       <input type="hidden" name="formPassword" value="<% formPassword|html>" />
+       <input type="hidden" name="returnPage" value="<% request.uri|html>" />
+       <input type="hidden" name="type" value="post" />
+       <input type="hidden" name="id" value="<%foreach posts post><% post.id|html><%notlast> <%/notlast><%/foreach>" />
+       <button type="submit" name="mark-read" value="true"><%= Notification.NewPost.Button.MarkRead|l10n|html></button>
+</form>
 <div class="short-text hidden">
        <%= Notification.NewPost.ShortText|l10n|html>
        <a class="link" onclick="showNotificationDetails('<%notification.id|html>'); return false;"><%= Notification.ClickHereToRead|l10n|html></a>
 </div>
 <div class="text">
-       <form class="mark-as-read" action="markAsKnown.html" method="post">
-               <input type="hidden" name="formPassword" value="<% formPassword|html>" />
-               <input type="hidden" name="returnPage" value="<% request.uri|html>" />
-               <input type="hidden" name="type" value="post" />
-               <input type="hidden" name="id" value="<%foreach posts post><% post.id|html><%notlast> <%/notlast><%/foreach>" />
-               <button type="submit" name="mark-read" value="true"><%= Notification.NewPost.Button.MarkRead|l10n|html></button>
-       </form>
        <%= Notification.NewPost.Text|l10n|html>
        <%foreach posts post>
                <div class="hidden post-id"><%post.id|html></div>
index ee8c410..a4588e5 100644 (file)
@@ -1,15 +1,15 @@
+<form class="mark-as-read" action="markAsKnown.html" method="post">
+       <input type="hidden" name="formPassword" value="<% formPassword|html>" />
+       <input type="hidden" name="returnPage" value="<% request.uri|html>" />
+       <input type="hidden" name="type" value="reply" />
+       <input type="hidden" name="id" value="<%foreach replies reply><% reply.id|html><%notlast> <%/notlast><%/foreach>" />
+       <button type="submit" name="mark-read" value="true"><%= Notification.NewPost.Button.MarkRead|l10n|html></button>
+</form>
 <div class="short-text hidden">
        <%= Notification.NewReply.ShortText|l10n|html>
        <a class="link" onclick="showNotificationDetails('<%notification.id|html>'); return false;"><%= Notification.ClickHereToRead|l10n|html></a>
 </div>
 <div class="text">
-       <form class="mark-as-read" action="markAsKnown.html" method="post">
-               <input type="hidden" name="formPassword" value="<% formPassword|html>" />
-               <input type="hidden" name="returnPage" value="<% request.uri|html>" />
-               <input type="hidden" name="type" value="reply" />
-               <input type="hidden" name="id" value="<%foreach replies reply><% reply.id|html><%notlast> <%/notlast><%/foreach>" />
-               <button type="submit" name="mark-read" value="true"><%= Notification.NewPost.Button.MarkRead|l10n|html></button>
-       </form>
        <%foreach replies reply><div class="hidden reply-id"><%reply.id|html></div><%/foreach>
        <%= Notification.NewReply.Text|l10n|html>
        <%foreach replies postGroup|replyGroup>
index aaa0d14..64985d8 100644 (file)
@@ -1,18 +1,18 @@
+<form class="mark-as-read" action="markAsKnown.html" method="post">
+       <input type="hidden" name="formPassword" value="<% formPassword|html>" />
+       <input type="hidden" name="returnPage" value="<% request.uri|html>" />
+       <input type="hidden" name="type" value="sone" />
+       <input type="hidden" name="id" value="<%foreach sones sone><% sone.id|html><%notlast> <%/notlast><%/foreach>" />
+       <button type="submit" name="mark-read" value="true"><%= Notification.NewPost.Button.MarkRead|l10n|html></button>
+</form>
 <div class="short-text hidden">
        <%= Notification.NewSone.ShortText|l10n|html>
        <a class="link" onclick="showNotificationDetails('<%notification.id|html>'); return false;"><%= Notification.ClickHereToRead|l10n|html></a>
 </div>
 <div class="text">
-       <form class="mark-as-read" action="markAsKnown.html" method="post">
-               <input type="hidden" name="formPassword" value="<% formPassword|html>" />
-               <input type="hidden" name="returnPage" value="<% request.uri|html>" />
-               <input type="hidden" name="type" value="sone" />
-               <input type="hidden" name="id" value="<%foreach sones sone><% sone.id|html><%notlast> <%/notlast><%/foreach>" />
-               <button type="submit" name="mark-read" value="true"><%= Notification.NewPost.Button.MarkRead|l10n|html></button>
-       </form>
        <%= Notification.NewSone.Text|l10n|html>
        <%foreach sones sone>
-               <div class="hidden sone-id"><% sone.id|html></div>
+               <div class="hidden new-sone-id"><% sone.id|html></div>
                <a href="viewSone.html?sone=<% sone.id|html>" title="<% sone.requestUri|html>"><% sone.niceName|html></a><%notlast>,<%/notlast><%last>.<%/last>
        <%/foreach>
 </div>
diff --git a/src/main/resources/templates/notify/rescuingSonesNotification.html b/src/main/resources/templates/notify/rescuingSonesNotification.html
deleted file mode 100644 (file)
index eef06ac..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-<div class="text">
-       <%= Notification.SoneIsBeingRescued.Text|l10n|html>
-       <%foreach sones sone>
-               <a href="viewSone.html?sone=<% sone.id|html>" title="<% sone.requestUri|html>"><% sone.niceName|html></a><%notlast>,<%/notlast><%last>.<%/last>
-       <%/foreach>
-</div>
diff --git a/src/main/resources/templates/notify/soneInsertNotification.html b/src/main/resources/templates/notify/soneInsertNotification.html
new file mode 100644 (file)
index 0000000..27374ad
--- /dev/null
@@ -0,0 +1,7 @@
+<%if soneStatus|match value="inserting">
+       Your Sone <a href="viewSone.html?sone=<%insertSone.id|html>"><%insertSone.niceName|html></a> is now being inserted.
+<%elseif soneStatus|match value="inserted">
+       Your Sone <a href="viewSone.html?sone=<%insertSone.id|html>"><%insertSone.niceName|html></a> has been inserted in <%= Notification.SoneInsert.Duration|l10n 0=insertDuration>.
+<%elseif soneStatus|match value="insert-aborted">
+       Inserting your Sone <a href="viewSone.html?sone=<%insertSone.id|html>"><%insertSone.niceName|html></a> has failed.
+<%/if>
\ No newline at end of file
diff --git a/src/main/resources/templates/notify/sonesRescuedNotification.html b/src/main/resources/templates/notify/sonesRescuedNotification.html
deleted file mode 100644 (file)
index 84f015d..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-<div class="text">
-       <%= Notification.SoneRescued.Text|l10n|html>
-       <%foreach sones sone>
-               <a href="viewSone.html?sone=<% sone.id|html>" title="<% sone.requestUri|html>"><% sone.niceName|html></a><%notlast>,<%/notlast><%last>.<%/last>
-       <%/foreach>
-       <%= Notification.SoneRescued.Text.RememberToUnlock|l10n|html>
-</div>
index db5eb80..39d579f 100644 (file)
@@ -8,6 +8,9 @@
                        getTranslation("WebInterface.DefaultText.Option.PostsPerPage", function(postsPerPageText) {
                                registerInputTextareaSwap("#sone #options input[name=posts-per-page]", postsPerPageText, "posts-per-page", true, true);
                        });
+                       getTranslation("WebInterface.DefaultText.Option.CharactersPerPost", function(postsPerPageText) {
+                               registerInputTextareaSwap("#sone #options input[name=characters-per-post]", postsPerPageText, "characters-per-post", true, true);
+                       });
                        getTranslation("WebInterface.DefaultText.Option.PositiveTrust", function(positiveTrustText) {
                                registerInputTextareaSwap("#sone #options input[name=positive-trust]", positiveTrustText, "positive-trust", true, true);
                        });
                        <%= Page.Options.Option.AutoFollow.Description|l10n|html>
                </p>
 
+               <p>
+                       <input type="checkbox" name="enable-sone-insert-notifications"<%ifnull currentSone> disabled="disabled"<%/if><%if enable-sone-insert-notifications> checked="checked"<%/if> />
+                       <%= Page.Options.Option.EnableSoneInsertNotifications.Description|l10n|html>
+               </p>
+
                <h2><%= Page.Options.Section.RuntimeOptions.Title|l10n|html></h2>
 
                <p><%= Page.Options.Option.InsertionDelay.Description|l10n|html></p>
+               <%if =insertion-delay|in collection=fieldErrors>
+                       <p class="warning"><%= Page.Options.Warnings.ValueNotChanged|l10n|html></p>
+               <%/if>
                <p><input type="text" name="insertion-delay" value="<% insertion-delay|html>" /></p>
 
                <p><%= Page.Options.Option.PostsPerPage.Description|l10n|html></p>
+               <%if =posts-per-page|in collection=fieldErrors>
+                       <p class="warning"><%= Page.Options.Warnings.ValueNotChanged|l10n|html></p>
+               <%/if>
                <p><input type="text" name="posts-per-page" value="<% posts-per-page|html>" /></p>
 
+               <p><%= Page.Options.Option.CharactersPerPost.Description|l10n|html></p>
+               <%if =characters-per-post|in collection=fieldErrors>
+                       <p class="warning"><%= Page.Options.Warnings.ValueNotChanged|l10n|html></p>
+               <%/if>
+               <p><input type="text" name="characters-per-post" value="<% characters-per-post|html>" /></p>
+
+               <p>
+                       <input type="checkbox" name="require-full-access"<%if require-full-access> checked="checked"<%/if> />
+                       <%= Page.Options.Option.RequireFullAccess.Description|l10n|html></p>
+               </p>
+
                <h2><%= Page.Options.Section.TrustOptions.Title|l10n|html></h2>
 
                <p><%= Page.Options.Option.PositiveTrust.Description|l10n|html></p>
+               <%if =positive-trust|in collection=fieldErrors>
+                       <p class="warning"><%= Page.Options.Warnings.ValueNotChanged|l10n|html></p>
+               <%/if>
                <p><input type="text" name="positive-trust" value="<% positive-trust|html>" /></p>
 
                <p><%= Page.Options.Option.NegativeTrust.Description|l10n|html></p>
+               <%if =negative-trust|in collection=fieldErrors>
+                       <p class="warning"><%= Page.Options.Warnings.ValueNotChanged|l10n|html></p>
+               <%/if>
                <p><input type="text" name="negative-trust" value="<% negative-trust|html>" /></p>
 
                <p><%= Page.Options.Option.TrustComment.Description|l10n|html></p>
                <p><input type="text" name="trust-comment" value="<% trust-comment|html>" /></p>
 
-               <h2><%= Page.Options.Section.RescueOptions.Title|l10n|html></h2>
+               <h2><%= Page.Options.Section.FcpOptions.Title|l10n|html></h2>
+
+               <p><input type="checkbox" name="fcp-interface-active"<%if fcp-interface-active> checked="checked"<%/if> /> <%= Page.Options.Option.FcpInterfaceActive.Description|l10n|html></p>
 
-               <p><%= Page.Options.Option.SoneRescueMode.Description|l10n|html></p>
-               <p><select name="sone-rescue-mode"><option disabled="disabled"><%= WebInterface.SelectBox.Choose|l10n|html></option><option value="true"<%if sone-rescue-mode> selected="selected"<%/if>><%= WebInterface.SelectBox.Yes|l10n|html></option><option value="false"<%if !sone-rescue-mode> selected="selected"<%/if>><%= WebInterface.SelectBox.No|l10n|html></option></select>
+               <p>
+                       <%= Page.Options.Option.FcpFullAccessRequired.Description|l10n|html|replace needle="{link}" replacement='<a href="/config/fcp">'|replace needle="{/link}" replacement='</a>'>
+                       <select name="fcp-full-access-required">
+                               <option value="0"<%if fcp-full-access-required|match value="0"> selected="selected"<%/if>><%= Page.Options.Option.FcpFullAccessRequired.Value.No|l10n|html></option>
+                               <option value="1"<%if fcp-full-access-required|match value="1"> selected="selected"<%/if>><%= Page.Options.Option.FcpFullAccessRequired.Value.Writing|l10n|html></option>
+                               <option value="2"<%if fcp-full-access-required|match value="2"> selected="selected"<%/if>><%= Page.Options.Option.FcpFullAccessRequired.Value.Always|l10n|html></option>
+                       </select>
+               </p>
 
                <h2><%= Page.Options.Section.Cleaning.Title|l10n|html></h2>
 
diff --git a/src/main/resources/templates/rescue.html b/src/main/resources/templates/rescue.html
new file mode 100644 (file)
index 0000000..e08edc3
--- /dev/null
@@ -0,0 +1,32 @@
+<%include include/head.html>
+
+<h1><%= Page.Rescue.Page.Title|l10n 0=currentSone.niceName|html></h1>
+
+<p><%= Page.Rescue.Text.Description|l10n|html></p>
+<p><%= Page.Rescue.Text.Procedure|l10n|html></p>
+
+<%if soneRescuer.fetching>
+       <p><%= Page.Rescue.Text.Fetching|l10n 0=soneRescuer.currentEdition|html></p>
+<%else>
+       <%if soneRescuer.hasNextEdition>
+               <%if soneRescuer.lastFetchSuccessful>
+                       <p><%= Page.Rescue.Text.Fetched|l10n 0=soneRescuer.currentEdition|html></p>
+               <%else>
+                       <p><%= Page.Rescue.Text.NotFetched|l10n 0=soneRescuer.currentEdition|html></p>
+               <%/if>
+               <form action="rescue.html" method="post">
+                       <input type="hidden" name="formPassword" value="<%formPassword|html>" />
+                       <label><%= Page.Rescue.Label.NextEdition|l10n|html></label>
+                       <input type="field" name="edition" value="<%soneRescuer.nextEdition>" />
+                       <button type="submit" name="fetch" value="true"><%= Page.Rescue.Button.Fetch|l10n|html></button>
+               </form>
+       <%else>
+               <%if soneRescuer.lastFetchSuccessful>
+                       <p><%= Page.Rescue.Text.Fetched|l10n 0=soneRescuer.currentEdition|html></p>
+               <%else>
+                       <p><%= Page.Rescue.Text.FetchedLast|l10n|html></p>
+               <%/if>
+       <%/if>
+<%/if>
+
+<%include include/tail.html>
index 87cf400..d80818a 100644 (file)
@@ -7,13 +7,17 @@
 
                <h1><%= Page.ViewSone.Page.TitleWithoutSone|l10n|html></h1>
 
-               <p><%= Page.ViewSone.NoSone.Description|l10n|replace needle="{sone}" replacementKey=sone.id|html></p>
+               <p><%= Page.ViewSone.NoSone.Description|l10n|replace needle="{sone}" replacementKey=soneId|html></p>
 
        <%elseifnull sone.name>
 
                <h1><%= Page.ViewSone.Page.TitleWithoutSone|l10n|html></h1>
 
                <p><%= Page.ViewSone.UnknownSone.Description|l10n|html></p>
+               <p>
+                       <%= Page.ViewSone.UnknownSone.LinkToWebOfTrust|l10n|html>
+                       <a href="/WebOfTrust/ShowIdentity?id=<% sone.id|html>"><%= Page.ViewSone.Profile.Name.WoTLink|l10n|html></a>
+               </p>
 
        <%else>
 
@@ -25,7 +29,7 @@
 
                        <div class="profile-field">
                                <div class="name"><%= Page.ViewSone.Profile.Label.Name|l10n|html></div>
-                               <div class="value"><a href="/WebOfTrust/ShowIdentity?id=<% sone.id|html>"><% sone.niceName|html></a></div>
+                               <div class="value"><% sone.niceName|html> (<a href="/WebOfTrust/ShowIdentity?id=<% sone.id|html>"><%= Page.ViewSone.Profile.Name.WoTLink|l10n|html></a>)</div>
                        </div>
 
                        <%foreach sone.albums album>
@@ -76,7 +80,7 @@
                <%foreach posts post>
                        <%first>
                                <div id="posts">
-                                       <%include include/pagination.html pagination=postPagination pageParameter==postPage>
+                                       <%include include/pagination.html pagination=postPagination pageParameter==postPage paginationName==post-navigation>
                        <%/first>
                        <%include include/viewPost.html>
                        <%last>
@@ -89,9 +93,9 @@
 
                <%foreach repliedPosts post>
                        <%first>
-                               <h2><%= Page.ViewSone.Replies.Title|l10n|html></h2>
+                               <h2><%= Page.ViewSone.Replies.Title|l10n|html|replace needle="{sone}" replacementKey=sone.niceName></h2>
                                <div id="replied-posts">
-                                       <%include include/pagination.html pagination=repliedPostPagination pageParameter==repliedPostPage>
+                                       <%include include/pagination.html pagination=repliedPostPagination pageParameter==repliedPostPage paginationName==reply-navigation>
                        <%/first>
                        <%include include/viewPost.html>
                        <%last>
diff --git a/src/test/java/net/pterodactylus/sone/text/FreenetLinkParserTest.java b/src/test/java/net/pterodactylus/sone/text/FreenetLinkParserTest.java
deleted file mode 100644 (file)
index 7e05fd5..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Sone - FreenetLinkParserTest.java - Copyright © 2010 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.text;
-
-import java.io.IOException;
-import java.io.StringReader;
-
-import junit.framework.TestCase;
-import net.pterodactylus.util.template.HtmlFilter;
-import net.pterodactylus.util.template.TemplateContextFactory;
-
-/**
- * JUnit test case for {@link FreenetLinkParser}.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class FreenetLinkParserTest extends TestCase {
-
-       /**
-        * Tests the parser.
-        *
-        * @throws IOException
-        *             if an I/O error occurs
-        */
-       public void testParser() throws IOException {
-               TemplateContextFactory templateContextFactory = new TemplateContextFactory();
-               templateContextFactory.addFilter("html", new HtmlFilter());
-               FreenetLinkParser parser = new FreenetLinkParser(null, templateContextFactory);
-               FreenetLinkParserContext context = new FreenetLinkParserContext(null);
-               Part part;
-
-               part = parser.parse(context, new StringReader("Text."));
-               assertEquals("Text.", part.toString());
-
-               part = parser.parse(context, new StringReader("Text.\nText."));
-               assertEquals("Text.\nText.", part.toString());
-
-               part = parser.parse(context, new StringReader("Text.\n\nText."));
-               assertEquals("Text.\n\nText.", part.toString());
-
-               part = parser.parse(context, new StringReader("Text.\n\n\nText."));
-               assertEquals("Text.\n\nText.", part.toString());
-
-               part = parser.parse(context, new StringReader("\nText.\n\n\nText."));
-               assertEquals("Text.\n\nText.", part.toString());
-
-               part = parser.parse(context, new StringReader("\nText.\n\n\nText.\n"));
-               assertEquals("Text.\n\nText.", part.toString());
-
-               part = parser.parse(context, new StringReader("\nText.\n\n\nText.\n\n"));
-               assertEquals("Text.\n\nText.", part.toString());
-
-               part = parser.parse(context, new StringReader("\nText.\n\n\n\nText.\n\n\n"));
-               assertEquals("Text.\n\nText.", part.toString());
-
-               part = parser.parse(context, new StringReader("\n\nText.\n\n\n\nText.\n\n\n"));
-               assertEquals("Text.\n\nText.", part.toString());
-
-               part = parser.parse(context, new StringReader("\n\nText. KSK@a text.\n\n\n\nText.\n\n\n"));
-               assertEquals("Text. <a class=\"freenet\" href=\"/KSK@a\" title=\"KSK@a\">a</a> text.\n\nText.", part.toString());
-       }
-
-}
diff --git a/src/test/java/net/pterodactylus/sone/text/SoneTextParserTest.java b/src/test/java/net/pterodactylus/sone/text/SoneTextParserTest.java
new file mode 100644 (file)
index 0000000..d98d4ec
--- /dev/null
@@ -0,0 +1,178 @@
+/*
+ * Sone - SoneTextParserTest.java - Copyright © 2011 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.text;
+
+import java.io.IOException;
+import java.io.StringReader;
+
+import junit.framework.TestCase;
+import net.pterodactylus.sone.core.SoneProvider;
+import net.pterodactylus.sone.data.Sone;
+
+/**
+ * JUnit test case for {@link SoneTextParser}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class SoneTextParserTest extends TestCase {
+
+       //
+       // ACTIONS
+       //
+
+       /**
+        * Tests basic plain-text operation of the parser.
+        *
+        * @throws IOException
+        *             if an I/O error occurs
+        */
+       public void testPlainText() throws IOException {
+               SoneTextParser soneTextParser = new SoneTextParser(null, null);
+               Iterable<Part> parts;
+
+               /* check basic operation. */
+               parts = soneTextParser.parse(null, new StringReader("Test."));
+               assertNotNull("Parts", parts);
+               assertEquals("Part Text", "Test.", convertText(parts, PlainTextPart.class));
+
+               /* check empty lines at start and end. */
+               parts = soneTextParser.parse(null, new StringReader("\nTest.\n\n"));
+               assertNotNull("Parts", parts);
+               assertEquals("Part Text", "Test.", convertText(parts, PlainTextPart.class));
+
+               /* check duplicate empty lines in the text. */
+               parts = soneTextParser.parse(null, new StringReader("\nTest.\n\n\nTest."));
+               assertNotNull("Parts", parts);
+               assertEquals("Part Text", "Test.\n\nTest.", convertText(parts, PlainTextPart.class));
+       }
+
+       /**
+        * Tests parsing of KSK links.
+        *
+        * @throws IOException
+        *             if an I/O error occurs
+        */
+       public void testKSKLinks() throws IOException {
+               SoneTextParser soneTextParser = new SoneTextParser(null, null);
+               Iterable<Part> parts;
+
+               /* check basic links. */
+               parts = soneTextParser.parse(null, new StringReader("KSK@gpl.txt"));
+               assertNotNull("Parts", parts);
+               assertEquals("Part Text", "[KSK@gpl.txt|gpl.txt|gpl.txt]", convertText(parts, FreenetLinkPart.class));
+
+               /* check embedded links. */
+               parts = soneTextParser.parse(null, new StringReader("Link is KSK@gpl.txt\u200b."));
+               assertNotNull("Parts", parts);
+               assertEquals("Part Text", "Link is [KSK@gpl.txt|gpl.txt|gpl.txt]\u200b.", convertText(parts, PlainTextPart.class, FreenetLinkPart.class));
+
+               /* check embedded links and line breaks. */
+               parts = soneTextParser.parse(null, new StringReader("Link is KSK@gpl.txt\nKSK@test.dat\n"));
+               assertNotNull("Parts", parts);
+               assertEquals("Part Text", "Link is [KSK@gpl.txt|gpl.txt|gpl.txt]\n[KSK@test.dat|test.dat|test.dat]", convertText(parts, PlainTextPart.class, FreenetLinkPart.class));
+       }
+
+       /**
+        * Test case for a bug that was discovered in 0.6.7.
+        *
+        * @throws IOException
+        *             if an I/O error occurs
+        */
+       @SuppressWarnings("synthetic-access")
+       public void testEmptyLinesAndSoneLinks() throws IOException {
+               SoneTextParser soneTextParser = new SoneTextParser(new TestSoneProvider(), null);
+               Iterable<Part> parts;
+
+               /* check basic links. */
+               parts = soneTextParser.parse(null, new StringReader("Some text.\n\nLink to sone://DAxKQzS48mtaQc7sUVHIgx3fnWZPQBz0EueBreUVWrU and stuff."));
+               assertNotNull("Parts", parts);
+               assertEquals("Part Text", "Some text.\n\nLink to [Sone|DAxKQzS48mtaQc7sUVHIgx3fnWZPQBz0EueBreUVWrU] and stuff.", convertText(parts, PlainTextPart.class, SonePart.class));
+       }
+
+       //
+       // PRIVATE METHODS
+       //
+
+       /**
+        * Converts all given {@link Part}s into a string, validating that the
+        * part’s classes match only the expected classes.
+        *
+        * @param parts
+        *            The parts to convert to text
+        * @param validClasses
+        *            The valid classes; if no classes are given, all classes are
+        *            valid
+        * @return The converted text
+        */
+       private String convertText(Iterable<Part> parts, Class<?>... validClasses) {
+               StringBuilder text = new StringBuilder();
+               for (Part part : parts) {
+                       assertNotNull("Part", part);
+                       boolean classValid = validClasses.length == 0;
+                       for (Class<?> validClass : validClasses) {
+                               if (validClass.isAssignableFrom(part.getClass())) {
+                                       classValid = true;
+                                       break;
+                               }
+                       }
+                       if (!classValid) {
+                               assertEquals("Part’s Class", null, part.getClass());
+                       }
+                       if (part instanceof PlainTextPart) {
+                               text.append(((PlainTextPart) part).getText());
+                       } else if (part instanceof FreenetLinkPart) {
+                               FreenetLinkPart freenetLinkPart = (FreenetLinkPart) part;
+                               text.append('[').append(freenetLinkPart.getLink()).append('|').append(freenetLinkPart.isTrusted() ? "trusted|" : "").append(freenetLinkPart.getTitle()).append('|').append(freenetLinkPart.getText()).append(']');
+                       } else if (part instanceof LinkPart) {
+                               LinkPart linkPart = (LinkPart) part;
+                               text.append('[').append(linkPart.getLink()).append('|').append(linkPart.getTitle()).append('|').append(linkPart.getText()).append(']');
+                       } else if (part instanceof SonePart) {
+                               SonePart sonePart = (SonePart) part;
+                               text.append("[Sone|").append(sonePart.getSone().getId()).append(']');
+                       }
+               }
+               return text.toString();
+       }
+
+       /**
+        * Mock Sone provider.
+        *
+        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+        */
+       private static class TestSoneProvider implements SoneProvider {
+
+               /**
+                * {@inheritDoc}
+                */
+               @Override
+               public Sone getSone(final String soneId, boolean create) {
+                       return new Sone(soneId) {
+
+                               /**
+                                * {@inheritDoc}
+                                */
+                               @Override
+                               public String getName() {
+                                       return soneId;
+                               }
+                       };
+               }
+
+       }
+
+}