Merge branch 'release-0.6.5' 0.6.5
authorDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Sun, 19 Jun 2011 14:27:08 +0000 (16:27 +0200)
committerDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Sun, 19 Jun 2011 14:27:08 +0000 (16:27 +0200)
62 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/Options.java
src/main/java/net/pterodactylus/sone/core/PostProvider.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/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/SimpleFieldSetBuilder.java [new file with mode: 0644]
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/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/ParserFilter.java
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/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
src/main/java/net/pterodactylus/sone/web/OptionsPage.java
src/main/java/net/pterodactylus/sone/web/SoneTemplatePage.java
src/main/java/net/pterodactylus/sone/web/WebInterface.java
src/main/java/net/pterodactylus/sone/web/ajax/GetNotificationAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/GetStatusAjaxPage.java
src/main/resources/i18n/sone.en.properties
src/main/resources/static/css/sone.css
src/main/resources/static/javascript/sone.js
src/main/resources/templates/include/head.html
src/main/resources/templates/include/viewPost.html
src/main/resources/templates/include/viewReply.html
src/main/resources/templates/notify/mentionNotification.html [new file with mode: 0644]
src/main/resources/templates/options.html
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 25a3f0d..36a51ff 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.4</version>
+       <version>0.6.5</version>
        <dependencies>
                <dependency>
                        <groupId>net.pterodactylus</groupId>
                        <artifactId>utils</artifactId>
-                       <version>0.9.5</version>
+                       <version>0.9.6</version>
                </dependency>
                <dependency>
                        <groupId>junit</groupId>
index 0685c9a..3c99eef 100644 (file)
@@ -40,6 +40,8 @@ import net.pterodactylus.sone.data.Profile;
 import net.pterodactylus.sone.data.Profile.Field;
 import net.pterodactylus.sone.data.Reply;
 import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.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;
@@ -47,12 +49,16 @@ import net.pterodactylus.sone.freenet.wot.OwnIdentity;
 import net.pterodactylus.sone.freenet.wot.Trust;
 import net.pterodactylus.sone.freenet.wot.WebOfTrustException;
 import net.pterodactylus.sone.main.SonePlugin;
+import net.pterodactylus.util.collection.Pair;
 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.thread.Ticker;
+import net.pterodactylus.util.validation.IntegerRangeValidator;
 import net.pterodactylus.util.validation.Validation;
 import net.pterodactylus.util.version.Version;
+import freenet.client.FetchResult;
 import freenet.keys.FreenetURI;
 
 /**
@@ -60,7 +66,7 @@ import freenet.keys.FreenetURI;
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
-public class Core implements IdentityListener, UpdateListener {
+public class Core implements IdentityListener, UpdateListener, SoneProvider, PostProvider {
 
        /**
         * Enumeration for the possible states of a {@link Sone}.
@@ -115,6 +121,9 @@ public class Core implements IdentityListener, UpdateListener {
        /** The update checker. */
        private final UpdateChecker updateChecker;
 
+       /** The FCP interface. */
+       private volatile FcpInterface fcpInterface;
+
        /** Whether the core has been stopped. */
        private volatile boolean stopped;
 
@@ -171,6 +180,9 @@ public class Core implements IdentityListener, UpdateListener {
        /** Trusted identities, sorted by own identities. */
        private Map<OwnIdentity, Set<Identity>> trustedIdentities = Collections.synchronizedMap(new HashMap<OwnIdentity, Set<Identity>>());
 
+       /** Ticker for threads that mark own elements as known. */
+       private Ticker localElementTicker = new Ticker();
+
        /**
         * Creates a new core.
         *
@@ -257,6 +269,16 @@ public class Core implements IdentityListener, UpdateListener {
        }
 
        /**
+        * 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
@@ -334,6 +356,7 @@ public class Core implements IdentityListener, UpdateListener {
         * @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);
@@ -556,6 +579,7 @@ public class Core implements IdentityListener, UpdateListener {
         *            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);
@@ -851,6 +875,14 @@ public class Core implements IdentityListener, UpdateListener {
                                        coreListenerManager.fireRescuingSone(sone);
                                        lockSone(sone);
                                        long edition = sone.getLatestEdition();
+                                       /* find the latest edition the node knows about. */
+                                       Pair<FreenetURI, FetchResult> currentUri = freenetInterface.fetchUri(sone.getRequestUri());
+                                       if (currentUri != null) {
+                                               long currentEdition = currentUri.getLeft().getEdition();
+                                               if (currentEdition > edition) {
+                                                       edition = currentEdition;
+                                               }
+                                       }
                                        while (!stopped && (edition >= 0) && preferences.isSoneRescueMode()) {
                                                logger.log(Level.FINE, "Downloading edition " + edition + "…");
                                                soneDownloader.fetchSone(sone, sone.getRequestUri().setKeyType("SSK").setDocName("Sone-" + edition));
@@ -917,6 +949,7 @@ public class Core implements IdentityListener, UpdateListener {
                                        for (Sone localSone : getLocalSones()) {
                                                if (localSone.getOptions().getBooleanOption("AutoFollow").get()) {
                                                        localSone.addFriend(sone.getId());
+                                                       saveSone(localSone);
                                                }
                                        }
                                }
@@ -1473,7 +1506,7 @@ public class Core implements IdentityListener, UpdateListener {
                        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);
                }
@@ -1486,6 +1519,16 @@ public class Core implements IdentityListener, UpdateListener {
                }
                sone.addPost(post);
                saveSone(sone);
+               localElementTicker.registerEvent(System.currentTimeMillis() + 10 * 1000, new Runnable() {
+
+                       /**
+                        * {@inheritDoc}
+                        */
+                       @Override
+                       public void run() {
+                               markPostKnown(post);
+                       }
+               }, "Mark " + post + " read.");
                return post;
        }
 
@@ -1606,7 +1649,7 @@ public class Core implements IdentityListener, UpdateListener {
                        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);
                }
@@ -1616,6 +1659,16 @@ public class Core implements IdentityListener, UpdateListener {
                }
                sone.addReply(reply);
                saveSone(sone);
+               localElementTicker.registerEvent(System.currentTimeMillis() + 10 * 1000, new Runnable() {
+
+                       /**
+                        * {@inheritDoc}
+                        */
+                       @Override
+                       public void run() {
+                               markReplyKnown(reply);
+                       }
+               }, "Mark " + reply + " read.");
                return reply;
        }
 
@@ -1676,6 +1729,9 @@ public class Core implements IdentityListener, UpdateListener {
                        for (SoneInserter soneInserter : soneInserters.values()) {
                                soneInserter.stop();
                        }
+                       for (Sone localSone : localSones.values()) {
+                               saveSone(localSone);
+                       }
                }
                updateChecker.stop();
                updateChecker.removeUpdateListener(this);
@@ -1705,6 +1761,8 @@ public class Core implements IdentityListener, UpdateListener {
                        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());
@@ -1767,7 +1825,7 @@ public class Core implements IdentityListener, UpdateListener {
        @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) {
@@ -1775,11 +1833,28 @@ public class Core implements IdentityListener, UpdateListener {
                        }
 
                }));
-               options.addIntegerOption("PostsPerPage", new DefaultOption<Integer>(10));
+               options.addIntegerOption("PostsPerPage", new DefaultOption<Integer>(10, new IntegerRangeValidator(1, Integer.MAX_VALUE)));
                options.addBooleanOption("RequireFullAccess", new DefaultOption<Boolean>(false));
-               options.addIntegerOption("PositiveTrust", new DefaultOption<Integer>(75));
-               options.addIntegerOption("NegativeTrust", new DefaultOption<Integer>(-25));
+               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));
@@ -1795,12 +1870,14 @@ public class Core implements IdentityListener, UpdateListener {
                        return;
                }
 
-               options.getIntegerOption("InsertionDelay").set(configuration.getIntValue("Option/InsertionDelay").getValue(null));
-               options.getIntegerOption("PostsPerPage").set(configuration.getIntValue("Option/PostsPerPage").getValue(null));
+               loadConfigurationValue("InsertionDelay");
+               loadConfigurationValue("PostsPerPage");
                options.getBooleanOption("RequireFullAccess").set(configuration.getBooleanValue("Option/RequireFullAccess").getValue(null));
-               options.getIntegerOption("PositiveTrust").set(configuration.getIntValue("Option/PositiveTrust").getValue(null));
-               options.getIntegerOption("NegativeTrust").set(configuration.getIntValue("Option/NegativeTrust").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. */
@@ -1854,6 +1931,21 @@ public class Core implements IdentityListener, UpdateListener {
        }
 
        /**
+        * 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
@@ -1971,6 +2063,7 @@ public class Core implements IdentityListener, UpdateListener {
                }
                synchronized (newSones) {
                        newSones.remove(identity.getId());
+                       coreListenerManager.fireSoneRemoved(sone);
                }
        }
 
@@ -2017,6 +2110,18 @@ public class Core implements IdentityListener, UpdateListener {
                }
 
                /**
+                * 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
@@ -2039,6 +2144,18 @@ public class Core implements IdentityListener, UpdateListener {
                }
 
                /**
+                * 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
@@ -2081,6 +2198,18 @@ public class Core implements IdentityListener, UpdateListener {
                }
 
                /**
+                * 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
@@ -2103,6 +2232,18 @@ public class Core implements IdentityListener, UpdateListener {
                }
 
                /**
+                * 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
@@ -2139,6 +2280,57 @@ public class Core implements IdentityListener, UpdateListener {
                }
 
                /**
+                * Returns whether the {@link FcpInterface FCP interface} is currently
+                * active.
+                *
+                * @see FcpInterface#setActive(boolean)
+                * @return {@code true} if the FCP interface is currently active,
+                *         {@code false} otherwise
+                */
+               public boolean isFcpInterfaceActive() {
+                       return options.getBooleanOption("ActivateFcpInterface").get();
+               }
+
+               /**
+                * Sets whether the {@link FcpInterface FCP interface} is currently
+                * active.
+                *
+                * @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 setFcpFullAccessRequired(FullAccessRequired fcpFullAccessRequired) {
+                       options.getIntegerOption("FcpFullAccessRequired").set((fcpFullAccessRequired != null) ? fcpFullAccessRequired.ordinal() : null);
+                       return this;
+               }
+
+               /**
                 * Returns whether the rescue mode is active.
                 *
                 * @return {@code true} if the rescue mode is active, {@code false}
index fb83c9f..d34ac36 100644 (file)
@@ -97,6 +97,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
index 6dbdc58..786cfb8 100644 (file)
@@ -147,6 +147,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)
index 94fbec1..1bdf21f 100644 (file)
@@ -24,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.
  *
@@ -64,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;
 
        }
 
@@ -113,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>>();
 
@@ -125,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));
                }
 
@@ -160,7 +194,18 @@ public class Options {
                 * {@inheritDoc}
                 */
                @Override
+               public boolean validate(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 0abebc3..cc5f689 100644 (file)
@@ -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;
@@ -265,8 +267,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()));
                }
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);
+
+}
index 715d95b..e1c0391 100644 (file)
@@ -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;
@@ -67,6 +69,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);
 
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());
+       }
+
+}
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..1d57454
--- /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(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;
+       }
+
+}
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 f27cc6e..b5a7fcc 100644 (file)
@@ -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, 4);
+       public static final Version VERSION = new Version(0, 6, 5);
 
        /** 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;
 
@@ -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 3a79c93..74e3100 100644 (file)
@@ -110,6 +110,7 @@ public class ListNotification<T> extends TemplateNotification {
        public void setElements(Collection<? extends T> elements) {
                this.elements.clear();
                this.elements.addAll(elements);
+               touch();
        }
 
        /**
index 2362d6a..37cff6f 100644 (file)
@@ -59,7 +59,7 @@ public class ListNotificationFilters {
                                if (filteredNotification != null) {
                                        filteredNotifications.add(filteredNotification);
                                }
-                       } else if (notification.getId().equals("new-replies-notification")) {
+                       } else if (notification.getId().equals("new-reply-notification")) {
                                ListNotification<Reply> filteredNotification = filterNewReplyNotification((ListNotification<Reply>) notification, currentSone);
                                if (filteredNotification != null) {
                                        filteredNotifications.add(filteredNotification);
index db945b5..26afe2e 100644 (file)
@@ -19,19 +19,29 @@ package net.pterodactylus.sone.template;
 
 import java.io.IOException;
 import java.io.StringReader;
+import java.io.StringWriter;
+import java.io.Writer;
 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.Page.Request;
 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>
  */
@@ -41,20 +51,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;
        }
 
        /**
@@ -71,13 +93,164 @@ public class ParserFilter implements Filter {
                if (sone == null) {
                        sone = core.getSone(soneKey, false);
                }
-               FreenetLinkParserContext context = new FreenetLinkParserContext((Request) templateContext.get("request"), sone);
+               Request request = (Request) templateContext.get("request");
+               SoneTextParserContext context = new SoneTextParserContext(request, sone);
+               StringWriter parsedTextWriter = new StringWriter();
                try {
-                       return linkParser.parse(context, new StringReader(text));
+                       render(parsedTextWriter, soneTextParser.parse(context, new StringReader(text)));
                } 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) {
+               if (text.length() > length) {
+                       return text.substring(0, length) + "…";
                }
-               return null;
+               return text;
        }
 
 }
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 7cb44ce..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 = ((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) && (post.getSone() != 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 0712fc0..0000000
+++ /dev/null
@@ -1,69 +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;
-import net.pterodactylus.sone.web.page.Page.Request;
-
-/**
- * {@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 request being processed. */
-       private final Request 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 FreenetLinkParserContext(Request request, Sone postingSone) {
-               this.request = request;
-               this.postingSone = postingSone;
-       }
-
-       /**
-        * Returns the request that is currently being processed.
-        *
-        * @return The request being processed
-        */
-       public Request 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/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 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..384e8ae 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,69 @@ 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()) {
+                                       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..94c3db4
--- /dev/null
@@ -0,0 +1,307 @@
+/*
+ * 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;
+                       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;
+                               }
+                               if (linkType == LinkType.SONE) {
+                                       if (next > 0) {
+                                               parts.add(new PlainTextPart(line.substring(0, next)));
+                                       }
+                                       if (line.length() >= (next + 7 + 43)) {
+                                               String soneId = line.substring(next + 7, next + 50);
+                                               Sone sone = soneProvider.getSone(soneId, false);
+                                               if (sone != null) {
+                                                       parts.add(new SonePart(sone));
+                                               } else {
+                                                       parts.add(new PlainTextPart(line.substring(next, next + 50)));
+                                               }
+                                               line = line.substring(next + 50);
+                                       } else {
+                                               parts.add(new PlainTextPart(line.substring(next)));
+                                               line = "";
+                                       }
+                                       continue;
+                               }
+                               if (linkType == LinkType.POST) {
+                                       if (next > 0) {
+                                               parts.add(new PlainTextPart(line.substring(0, next)));
+                                       }
+                                       if (line.length() >= (next + 7 + 36)) {
+                                               String postId = line.substring(next + 7, next + 43);
+                                               Post post = postProvider.getPost(postId, false);
+                                               if ((post != null) && (post.getSone() != null)) {
+                                                       String postText = post.getText();
+                                                       postText = postText.substring(0, Math.min(postText.length(), 20)) + "…";
+                                                       parts.add(new PostPart(post));
+                                               } else {
+                                                       parts.add(new PlainTextPart(line.substring(next, next + 43)));
+                                               }
+                                               line = line.substring(next + 43);
+                                       } else {
+                                               parts.add(new PlainTextPart(line.substring(next)));
+                                               line = "";
+                                       }
+                                       continue;
+                               }
+                               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(new PlainTextPart("\n" + line.substring(0, next)));
+                                       } else {
+                                               if (next > 0) {
+                                                       parts.add(new PlainTextPart(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 = ((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);
+                               } else {
+                                       if (!lastLineEmpty && lineComplete) {
+                                               parts.add(new PlainTextPart("\n" + line.substring(0, next + 4)));
+                                       } else {
+                                               parts.add(new PlainTextPart(line.substring(0, next + 4)));
+                                       }
+                                       line = line.substring(next + 4);
+                               }
+                               lineComplete = false;
+                       }
+                       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..4a89016
--- /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.Page.Request;
+
+/**
+ * {@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 Request 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(Request request, Sone postingSone) {
+               this.request = request;
+               this.postingSone = postingSone;
+       }
+
+       /**
+        * Returns the request that is currently being processed.
+        *
+        * @return The request being processed
+        */
+       public Request 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();
-       }
-
-}
index 5744368..9e3587c 100644 (file)
 
 package net.pterodactylus.sone.text;
 
-import java.util.logging.Logger;
-
-import net.pterodactylus.util.logging.Logging;
-
 /**
  * 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”
@@ -32,9 +28,6 @@ import net.pterodactylus.util.logging.Logging;
  */
 public class TextFilter {
 
-       /** The logger. */
-       private static final Logger logger = Logging.getLogger(TextFilter.class);
-
        /**
         * Filters the given text, stripping the host header part for links to the
         * local node.
index bbc1084..6785e81 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.fcp.FcpInterface.FullAccessRequired;
 import net.pterodactylus.sone.web.page.Page.Request.Method;
 import net.pterodactylus.util.number.Numbers;
 import net.pterodactylus.util.template.Template;
@@ -56,26 +60,48 @@ public class OptionsPage extends SoneTemplatePage {
                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);
                        }
                        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);
+                       }
                        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 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 soneRescueMode = Boolean.parseBoolean(request.getHttpRequest().getPartAsStringFailsafe("sone-rescue-mode", 5));
                        preferences.setSoneRescueMode(soneRescueMode);
                        boolean clearOnNextRestart = Boolean.parseBoolean(request.getHttpRequest().getPartAsStringFailsafe("clear-on-next-restart", 5));
@@ -83,7 +109,10 @@ public class OptionsPage extends SoneTemplatePage {
                        boolean reallyClearOnNextRestart = Boolean.parseBoolean(request.getHttpRequest().getPartAsStringFailsafe("really-clear-on-next-restart", 5));
                        preferences.setReallyClearOnNextRestart(reallyClearOnNextRestart);
                        webInterface.getCore().saveConfiguration();
-                       throw new RedirectException(getPath());
+                       if (fieldErrors.isEmpty()) {
+                               throw new RedirectException(getPath());
+                       }
+                       templateContext.set("fieldErrors", fieldErrors);
                }
                if (currentSone != null) {
                        templateContext.set("auto-follow", currentSone.getOptions().getBooleanOption("AutoFollow").get());
@@ -94,6 +123,8 @@ public class OptionsPage extends SoneTemplatePage {
                templateContext.set("positive-trust", preferences.getPositiveTrust());
                templateContext.set("negative-trust", preferences.getNegativeTrust());
                templateContext.set("trust-comment", preferences.getTrustComment());
+               templateContext.set("fcp-interface-active", preferences.isFcpInterfaceActive());
+               templateContext.set("fcp-full-access-required", preferences.getFcpFullAccessRequired().ordinal());
                templateContext.set("sone-rescue-mode", preferences.isSoneRescueMode());
                templateContext.set("clear-on-next-restart", preferences.isClearOnNextRestart());
                templateContext.set("really-clear-on-next-restart", preferences.isReallyClearOnNextRestart());
index 42c0129..a0174a9 100644 (file)
@@ -120,7 +120,6 @@ public class SoneTemplatePage extends FreenetTemplatePage {
                this.pageTitleKey = pageTitleKey;
                this.webInterface = webInterface;
                this.requireLogin = requireLogin;
-               template.getInitialContext().set("webInterface", webInterface);
        }
 
        //
index 871a2eb..73436c7 100644 (file)
@@ -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;
@@ -58,6 +59,9 @@ import net.pterodactylus.sone.template.SoneAccessor;
 import net.pterodactylus.sone.template.SubstringFilter;
 import net.pterodactylus.sone.template.TrustAccessor;
 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;
@@ -96,11 +100,14 @@ 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;
@@ -150,6 +157,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;
 
@@ -159,6 +169,15 @@ public class WebInterface implements CoreListener {
        /** The “new reply” notification. */
        private final ListNotification<Reply> newReplyNotification;
 
+       /** The invisible “local post” notification. */
+       private final ListNotification<Post> localPostNotification;
+
+       /** The invisible “local reply” notification. */
+       private final ListNotification<Reply> localReplyNotification;
+
+       /** The “you have been mentioned” notification. */
+       private final ListNotification<Post> mentionNotification;
+
        /** The “rescuing Sone” notification. */
        private final ListNotification<Sone> rescuingSonesNotification;
 
@@ -184,6 +203,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());
@@ -205,13 +225,15 @@ 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("replyGroup", new ReplyGroupFilter());
+               templateContextFactory.addFilter("in", new ContainsFilter());
                templateContextFactory.addProvider(Provider.TEMPLATE_CONTEXT_PROVIDER);
                templateContextFactory.addProvider(new ClassPathTemplateProvider());
+               templateContextFactory.addTemplateObject("webInterface", this);
                templateContextFactory.addTemplateObject("formPassword", formPassword);
 
                /* create notifications. */
@@ -221,8 +243,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 localReplyNotificationTemplate = TemplateParser.parse(createReader("/templates/notify/newReplyNotification.html"));
+               localReplyNotification = new ListNotification<Reply>("local-reply-notification", "replies", localReplyNotificationTemplate, false);
+
+               Template mentionNotificationTemplate = TemplateParser.parse(createReader("/templates/notify/mentionNotification.html"));
+               mentionNotification = new ListNotification<Post>("mention-notification", "posts", mentionNotificationTemplate, false);
 
                Template rescuingSonesTemplate = TemplateParser.parse(createReader("/templates/notify/rescuingSonesNotification.html"));
                rescuingSonesNotification = new ListNotification<Sone>("sones-being-rescued-notification", "sones", rescuingSonesTemplate);
@@ -402,7 +433,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();
        }
 
        /**
@@ -412,7 +443,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();
        }
 
        /**
@@ -646,11 +677,34 @@ public class WebInterface implements CoreListener {
                try {
                        return new InputStreamReader(getClass().getResourceAsStream(resourceName), "UTF-8");
                } catch (UnsupportedEncodingException uee1) {
-                       System.out.println("  fail.");
                        return null;
                }
        }
 
+       /**
+        * 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
+        */
+       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);
+       }
+
        //
        // CORELISTENER METHODS
        //
@@ -690,9 +744,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);
                }
@@ -706,9 +769,18 @@ public class WebInterface implements CoreListener {
                if (reply.getPost().getSone() == null) {
                        return;
                }
-               newReplyNotification.add(reply);
+               boolean isLocal = getCore().isLocalSone(reply.getSone());
+               if (isLocal) {
+                       localReplyNotification.add(reply);
+               } else {
+                       newReplyNotification.add(reply);
+               }
                if (!hasFirstStartNotification()) {
-                       notificationManager.addNotification(newReplyNotification);
+                       notificationManager.addNotification(isLocal ? localReplyNotification : newReplyNotification);
+                       if (!getMentionedSones(reply.getText()).isEmpty() && !isLocal) {
+                               mentionNotification.add(reply.getPost());
+                               notificationManager.addNotification(mentionNotification);
+                       }
                } else {
                        getCore().markReplyKnown(reply);
                }
@@ -728,6 +800,8 @@ public class WebInterface implements CoreListener {
        @Override
        public void markPostKnown(Post post) {
                newPostNotification.remove(post);
+               localPostNotification.remove(post);
+               mentionNotification.remove(post);
        }
 
        /**
@@ -736,6 +810,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);
        }
 
        /**
@@ -744,6 +828,7 @@ public class WebInterface implements CoreListener {
        @Override
        public void postRemoved(Post post) {
                newPostNotification.remove(post);
+               localPostNotification.remove(post);
        }
 
        /**
@@ -752,6 +837,7 @@ public class WebInterface implements CoreListener {
        @Override
        public void replyRemoved(Reply reply) {
                newReplyNotification.remove(reply);
+               localReplyNotification.remove(reply);
        }
 
        /**
index 650a9ae..1406c3a 100644 (file)
@@ -19,7 +19,6 @@ package net.pterodactylus.sone.web.ajax;
 
 import java.io.IOException;
 import java.io.StringWriter;
-import java.util.ArrayList;
 
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Reply;
@@ -79,10 +78,9 @@ public class GetNotificationAjaxPage extends JsonPage {
        protected JsonObject createJsonObject(Request request) {
                String[] notificationIds = request.getHttpRequest().getParam("notifications").split(",");
                JsonObject jsonNotifications = new JsonObject();
-               Sone currentSone = webInterface.getCurrentSone(request.getToadletContext(), false);
+               Sone currentSone = getCurrentSone(request.getToadletContext(), false);
                for (String notificationId : notificationIds) {
                        Notification notification = webInterface.getNotifications().getNotification(notificationId);
-                       ListNotificationFilters.filterNotifications(new ArrayList<Notification>(), currentSone);
                        if ("new-post-notification".equals(notificationId)) {
                                notification = ListNotificationFilters.filterNewPostNotification((ListNotification<Post>) notification, currentSone);
                        } else if ("new-reply-notification".equals(notificationId)) {
index 4e2049e..246cbed 100644 (file)
@@ -19,7 +19,6 @@ package net.pterodactylus.sone.web.ajax;
 
 import java.text.DateFormat;
 import java.text.SimpleDateFormat;
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Date;
 import java.util.HashSet;
@@ -89,7 +88,7 @@ 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 jsonNotificationInformations = new JsonArray();
                for (Notification notification : notifications) {
@@ -128,6 +127,14 @@ public class GetStatusAjaxPage extends JsonPage {
 
                        });
                }
+               /* remove replies to unknown posts. */
+               newReplies = Filters.filteredSet(newReplies, new Filter<Reply>() {
+
+                       @Override
+                       public boolean filterObject(Reply reply) {
+                               return reply.getPost() != null;
+                       }
+               });
                JsonArray jsonReplies = new JsonArray();
                for (Reply reply : newReplies) {
                        JsonObject jsonReply = new JsonObject();
@@ -137,7 +144,7 @@ public class GetStatusAjaxPage extends JsonPage {
                        jsonReply.put("postSone", reply.getPost().getSone().getId());
                        jsonReplies.add(jsonReply);
                }
-               return createSuccessJsonObject().put("sones", jsonSones).put("notifications", jsonNotificationInformations).put("newPosts", jsonPosts).put("newReplies", jsonReplies);
+               return createSuccessJsonObject().put("loggedIn", currentSone != null).put("sones", jsonSones).put("notifications", jsonNotificationInformations).put("newPosts", jsonPosts).put("newReplies", jsonReplies);
        }
 
        /**
index 21ebc8f..3610afc 100644 (file)
@@ -43,6 +43,12 @@ 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.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.RescueOptions.Title=Rescue Settings
 Page.Options.Option.SoneRescueMode.Description1=Try to rescue your Sones at the next start of the Sone plugin. The Rescue Mode will start at the latest known edition and will try to download all editions sequentially backwards, merging all discovered posts and replies together, until it is stopped or it has reached the first edition.
 Page.Options.Option.SoneRescueMode.Description2=When using the Rescue Mode because Sone lost its configuration it usually suffices to let the Rescue Mode only run for a short time; use a second tab to control how many posts of your Sone are visible again. As soon as the last valid edition is loaded you can then deactivate the Rescue Mode.
@@ -50,6 +56,7 @@ Page.Options.Option.SoneRescueMode.Description3=Note that when you use the Rescu
 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
@@ -305,3 +312,5 @@ Notification.SoneRescued.Text=The following Sones have been rescued:
 Notification.SoneRescued.Text.RememberToUnlock=Please remember to control the posts and replies you have given and don’t forget to unlock your Sones!
 Notification.LockedSones.Text=The following Sones have been locked for more than 5 minutes. Please check if you really want to keep these Sones locked:
 Notification.NewVersion.Text=Version {version} of the Sone plugin was found. Download it from USK@nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI,DuQSUZiI~agF8c-6tjsFFGuZ8eICrzWCILB60nT8KKo,AQACAAE/sone/{edition}​!
+Notification.Mention.ShortText=You have been mentioned.
+Notification.Mention.Text=You have been mentioned in the following posts:
index 893072e..1dda47e 100644 (file)
@@ -143,6 +143,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;
@@ -264,6 +268,7 @@ textarea {
 #sone .post .text, #sone .post .raw-text {
        display: inline;
        white-space: pre-wrap;
+       word-wrap: break-word;
 }
 
 #sone .post .text.hidden, #sone .post .raw-text.hidden {
@@ -670,3 +675,8 @@ textarea {
        font-weight: bold;
        color: red;
 }
+
+#sone .warning {
+       color: red;
+       font-style: italic;
+}
index 9c082f1..d6c83ae 100644 (file)
@@ -8,6 +8,9 @@ function ajaxGet(url, data, successCallback, errorCallback) {
                                successCallback(data, textStatus);
                        }
                }, "error": function(xmlHttpRequest, textStatus, errorThrown) {
+                       if (xmlHttpRequest.status == 403) {
+                               notLoggedIn = true;
+                       }
                        if (typeof errorCallback != "undefined") {
                                errorCallback();
                        } else {
@@ -80,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);
@@ -863,7 +868,11 @@ function ajaxifyNotification(notification) {
                notification.find(".text").addClass("hidden");
        }
        notification.find("form.mark-as-read button").click(function() {
-               ajaxGet("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);
@@ -976,6 +985,10 @@ function getStatus() {
                        $.each(data.sones, function(index, value) {
                                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");
@@ -1070,8 +1083,10 @@ function loadNotifications(notificationIds) {
                                oldNotification.replaceWith(notification.show());
                        } else {
                                $("#sone #notification-area").append(notification);
-                               notification.slideDown();
-                               setActivity();
+                               if (value.id.substring(0, 5) != "local") {
+                                       notification.slideDown();
+                                       setActivity();
+                               }
                        }
                });
        });
@@ -1202,7 +1217,7 @@ function loadNewPost(postId, soneId, recipientId, time) {
        }
        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;
                        }
                }
@@ -1215,7 +1230,7 @@ function loadNewPost(postId, soneId, recipientId, time) {
                        if (hasPost(data.post.id)) {
                                return;
                        }
-                       if ((!isIndexPage() || (getPage(".pagination-index") > 1)) && !(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;
@@ -1226,6 +1241,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);
                        }
@@ -1259,6 +1277,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 {
@@ -1299,15 +1325,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") && !skipRequest)) {
                        (function(postElement) {
                                $(postElement).removeClass("new");
-                               $(".click-to-show", postElement).removeClass("new");
                                if ((typeof skipRequest == "undefined") || !skipRequest) {
                                        ajaxGet("markAsKnown.ajax", {"formPassword": getFormPassword(), "type": "post", "id": getPostId(postElement)});
                                }
                        })(postElement);
                }
+               $(".click-to-show", postElement).removeClass("new");
        });
        markReplyAsKnown($(postElements).find(".reply"));
 }
@@ -1315,7 +1341,7 @@ function markPostAsKnown(postElements, skipRequest) {
 function markReplyAsKnown(replyElements, skipRequest) {
        $(replyElements).each(function() {
                replyElement = this;
-               if ($(replyElement).hasClass("new")) {
+               if ($(replyElement).hasClass("new") || ((typeof skipRequest != "undefined") && !skipRequest)) {
                        (function(replyElement) {
                                $(replyElement).removeClass("new");
                                if ((typeof skipRequest == "undefined") || !skipRequest) {
@@ -1586,7 +1612,7 @@ var statusRequestQueued = true;
  */
 function ajaxError() {
        online = false;
-       toggleOfflineMarker(true);
+       showOfflineMarker(true);
        if (!statusRequestQueued) {
                setTimeout(getStatus, 5000);
                statusRequestQueued = true;
@@ -1598,7 +1624,7 @@ function ajaxError() {
  */
 function ajaxSuccess() {
        online = true;
-       toggleOfflineMarker(false);
+       showOfflineMarker(!online || (initiallyLoggedIn && notLoggedIn));
 }
 
 /**
@@ -1608,7 +1634,7 @@ function ajaxSuccess() {
  *            {@code true} to display the offline marker, {@code false} to hide
  *            it
  */
-function toggleOfflineMarker(visible) {
+function showOfflineMarker(visible) {
        /* jQuery documentation says toggle() works the other way around?! */
        $("#sone #offline-marker").toggle(visible);
        if (visible) {
@@ -1624,6 +1650,8 @@ function toggleOfflineMarker(visible) {
 
 var focus = true;
 var online = true;
+var initiallyLoggedIn = $("#sone #loggedIn").text() == "true";
+var notLoggedIn = !initiallyLoggedIn;
 
 $(document).ready(function() {
 
@@ -1709,7 +1737,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");
                                        }
index e80a92c..b37df26 100644 (file)
@@ -2,6 +2,7 @@
 
        <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>
index 8b7ccb8..b451f2b 100644 (file)
@@ -2,6 +2,7 @@
        <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="post-author-local hidden"><% post.sone.local></div>
        <div class="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" />
index 2fa4885..2507917 100644 (file)
@@ -2,6 +2,7 @@
        <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="reply-author-local hidden"><% reply.sone.local></div>
        <div class="avatar">
                <img src="/WebOfTrust/GetIdenticon?identity=<% reply.sone.id|html>&amp;width=36&height=36" width="36" height="36" alt="Avatar Image" />
        </div>
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..087fc2e
--- /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>
+               <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 c5130d7..368c14e 100644 (file)
                <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>
                <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.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.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.RescueOptions.Title|l10n|html></h2>
 
                <p><%= Page.Options.Option.SoneRescueMode.Description1|l10n|html></p>
index 689ff38..5c8b7ff 100644 (file)
@@ -63,7 +63,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>
@@ -78,7 +78,7 @@
                        <%first>
                                <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 e667aeb..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, 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..0343f05
--- /dev/null
@@ -0,0 +1,130 @@
+/*
+ * 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;
+
+/**
+ * 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));
+       }
+
+       //
+       // 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(']');
+                       }
+               }
+               return text.toString();
+       }
+
+}