Merge branch 'next' into image-management
authorDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Mon, 10 Jan 2011 19:16:25 +0000 (20:16 +0100)
committerDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Mon, 10 Jan 2011 19:16:25 +0000 (20:16 +0100)
26 files changed:
pom.xml
src/main/java/net/pterodactylus/sone/core/Core.java
src/main/java/net/pterodactylus/sone/core/CoreListener.java
src/main/java/net/pterodactylus/sone/core/CoreListenerManager.java
src/main/java/net/pterodactylus/sone/core/FreenetInterface.java
src/main/java/net/pterodactylus/sone/core/SoneDownloader.java
src/main/java/net/pterodactylus/sone/core/UpdateChecker.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/core/UpdateListener.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/core/UpdateListenerManager.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/main/SonePlugin.java
src/main/java/net/pterodactylus/sone/template/PostAccessor.java
src/main/java/net/pterodactylus/sone/template/ReplyAccessor.java
src/main/java/net/pterodactylus/sone/text/FreenetLinkParser.java
src/main/java/net/pterodactylus/sone/text/FreenetLinkParserContext.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/text/Parser.java
src/main/java/net/pterodactylus/sone/text/ParserContext.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/SoneTemplatePage.java
src/main/java/net/pterodactylus/sone/web/WebInterface.java
src/main/resources/i18n/sone.en.properties
src/main/resources/static/css/sone.css
src/main/resources/templates/include/tail.html
src/main/resources/templates/insert/sone.xml
src/main/resources/templates/notify/newPostNotification.html
src/main/resources/templates/notify/newReplyNotification.html
src/main/resources/templates/notify/newSoneNotification.html
src/main/resources/templates/notify/newVersionNotification.html [new file with mode: 0644]

diff --git a/pom.xml b/pom.xml
index 5fe7a4b..e9ff09c 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.3.6-2</version>
+       <version>0.3.6-5</version>
        <dependencies>
                <dependency>
                        <groupId>net.pterodactylus</groupId>
                        <artifactId>utils</artifactId>
-                       <version>0.7.6</version>
+                       <version>0.7.7</version>
                </dependency>
                <dependency>
                        <groupId>junit</groupId>
index 1a92901..78df9ef 100644 (file)
@@ -45,6 +45,7 @@ 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.version.Version;
 import freenet.keys.FreenetURI;
 
 /**
@@ -52,7 +53,7 @@ import freenet.keys.FreenetURI;
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
-public class Core implements IdentityListener {
+public class Core implements IdentityListener, UpdateListener {
 
        /**
         * Enumeration for the possible states of a {@link Sone}.
@@ -98,6 +99,9 @@ public class Core implements IdentityListener {
        /** The Sone downloader. */
        private final SoneDownloader soneDownloader;
 
+       /** The update checker. */
+       private final UpdateChecker updateChecker;
+
        /** Whether the core has been stopped. */
        private volatile boolean stopped;
 
@@ -162,6 +166,7 @@ public class Core implements IdentityListener {
                this.freenetInterface = freenetInterface;
                this.identityManager = identityManager;
                this.soneDownloader = new SoneDownloader(this, freenetInterface);
+               this.updateChecker = new UpdateChecker(freenetInterface);
        }
 
        //
@@ -233,6 +238,15 @@ public class Core implements IdentityListener {
        }
 
        /**
+        * Returns the update checker.
+        *
+        * @return The update checker
+        */
+       public UpdateChecker getUpdateChecker() {
+               return updateChecker;
+       }
+
+       /**
         * Returns the status of the given Sone.
         *
         * @param sone
@@ -1400,6 +1414,8 @@ public class Core implements IdentityListener {
         */
        public void start() {
                loadConfiguration();
+               updateChecker.addUpdateListener(this);
+               updateChecker.start();
        }
 
        /**
@@ -1411,6 +1427,8 @@ public class Core implements IdentityListener {
                                soneInserter.stop();
                        }
                }
+               updateChecker.stop();
+               updateChecker.removeUpdateListener(this);
                soneDownloader.stop();
                saveConfiguration();
                stopped = true;
@@ -1430,6 +1448,7 @@ public class Core implements IdentityListener {
 
                /* store the options first. */
                try {
+                       configuration.getIntValue("Option/ConfigurationVersion").setValue(0);
                        configuration.getIntValue("Option/InsertionDelay").setValue(options.getIntegerOption("InsertionDelay").getReal());
                        configuration.getBooleanValue("Option/SoneRescueMode").setValue(options.getBooleanOption("SoneRescueMode").getReal());
                        configuration.getBooleanValue("Option/ClearOnNextRestart").setValue(options.getBooleanOption("ClearOnNextRestart").getReal());
@@ -1621,4 +1640,16 @@ public class Core implements IdentityListener {
                /* TODO */
        }
 
+       //
+       // INTERFACE UpdateListener
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public void updateFound(Version version, long releaseTime) {
+               coreListenerManager.fireUpdateFound(version, releaseTime);
+       }
+
 }
index 5d5e715..5fbb333 100644 (file)
@@ -22,6 +22,7 @@ import java.util.EventListener;
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Reply;
 import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.util.version.Version;
 
 /**
  * Listener interface for objects that want to be notified on certain
@@ -127,4 +128,14 @@ public interface CoreListener extends EventListener {
         */
        public void soneUnlocked(Sone sone);
 
+       /**
+        * Notifies a listener that a new version has been found.
+        *
+        * @param version
+        *            The version that was found
+        * @param releaseTime
+        *            The release time of the new version
+        */
+       public void updateFound(Version version, long releaseTime);
+
 }
index 464342c..4fc9532 100644 (file)
@@ -21,6 +21,7 @@ import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Reply;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.util.event.AbstractListenerManager;
+import net.pterodactylus.util.version.Version;
 
 /**
  * Manager for {@link CoreListener}s.
@@ -197,4 +198,19 @@ public class CoreListenerManager extends AbstractListenerManager<Core, CoreListe
                }
        }
 
+       /**
+        * Notifies all listeners that a new version was found.
+        *
+        * @see CoreListener#updateFound(Version, long)
+        * @param version
+        *            The new version
+        * @param releaseTime
+        *            The release time of the new version
+        */
+       void fireUpdateFound(Version version, long releaseTime) {
+               for (CoreListener coreListener : getListeners()) {
+                       coreListener.updateFound(version, releaseTime);
+               }
+       }
+
 }
index 6737d63..52c2857 100644 (file)
@@ -18,6 +18,7 @@
 package net.pterodactylus.sone.core;
 
 import java.net.MalformedURLException;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.logging.Level;
@@ -60,6 +61,9 @@ public class FreenetInterface {
        /** The USK callbacks. */
        private final Map<String, USKCallback> soneUskCallbacks = new HashMap<String, USKCallback>();
 
+       /** The not-Sone-related USK callbacks. */
+       private final Map<FreenetURI, USKCallback> uriUskCallbacks = Collections.synchronizedMap(new HashMap<FreenetURI, USKCallback>());
+
        /**
         * Creates a new Freenet interface.
         *
@@ -197,4 +201,83 @@ public class FreenetInterface {
                }
        }
 
+       /**
+        * Registers an arbitrary URI and calls the given callback if a new edition
+        * is found.
+        *
+        * @param uri
+        *            The URI to watch
+        * @param callback
+        *            The callback to call
+        */
+       public void registerUsk(FreenetURI uri, final Callback callback) {
+               USKCallback uskCallback = new USKCallback() {
+
+                       @Override
+                       public void onFoundEdition(long edition, USK key, ObjectContainer objectContainer, ClientContext clientContext, boolean metadata, short codec, byte[] data, boolean newKnownGood, boolean newSlotToo) {
+                               callback.editionFound(key.getURI(), edition, newKnownGood, newSlotToo);
+                       }
+
+                       @Override
+                       public short getPollingPriorityNormal() {
+                               return RequestStarter.PREFETCH_PRIORITY_CLASS;
+                       }
+
+                       @Override
+                       public short getPollingPriorityProgress() {
+                               return RequestStarter.INTERACTIVE_PRIORITY_CLASS;
+                       }
+
+               };
+               try {
+                       node.clientCore.uskManager.subscribe(USK.create(uri), uskCallback, true, (HighLevelSimpleClientImpl) client);
+                       uriUskCallbacks.put(uri, uskCallback);
+               } catch (MalformedURLException mue1) {
+                       logger.log(Level.WARNING, "Could not subscribe to USK: " + uri, uri);
+               }
+       }
+
+       /**
+        * Unregisters the USK watcher for the given URI.
+        *
+        * @param uri
+        *            The URI to unregister the USK watcher for
+        */
+       public void unregisterUsk(FreenetURI uri) {
+               USKCallback uskCallback = uriUskCallbacks.remove(uri);
+               if (uskCallback == null) {
+                       logger.log(Level.INFO, "Could not unregister unknown USK: " + uri);
+                       return;
+               }
+               try {
+                       node.clientCore.uskManager.unsubscribe(USK.create(uri), uskCallback);
+               } catch (MalformedURLException mue1) {
+                       logger.log(Level.INFO, "Could not unregister invalid USK: " + uri);
+               }
+       }
+
+       /**
+        * Callback for USK watcher events.
+        *
+        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+        */
+       public static interface Callback {
+
+               /**
+                * Notifies a listener that a new edition was found for a URI.
+                *
+                * @param uri
+                *            The URI that a new edition was found for
+                * @param edition
+                *            The found edition
+                * @param newKnownGood
+                *            Whether the found edition was actually fetched
+                * @param newSlot
+                *            Whether the found edition is higher than all previously
+                *            found editions
+                */
+               public void editionFound(FreenetURI uri, long edition, boolean newKnownGood, boolean newSlot);
+
+       }
+
 }
index fa0063e..4328f02 100644 (file)
@@ -55,6 +55,9 @@ public class SoneDownloader extends AbstractService {
        /** The logger. */
        private static final Logger logger = Logging.getLogger(SoneDownloader.class);
 
+       /** The maximum protocol version. */
+       private static final int MAX_PROTOCOL_VERSION = 0;
+
        /** The core. */
        private final Core core;
 
@@ -221,6 +224,27 @@ public class SoneDownloader extends AbstractService {
                        return null;
                }
 
+               Integer protocolVersion = null;
+               String soneProtocolVersion = soneXml.getValue("protocol-version", null);
+               if (soneProtocolVersion != null) {
+                       protocolVersion = Numbers.safeParseInteger(soneProtocolVersion);
+               }
+               if (protocolVersion == null) {
+                       logger.log(Level.INFO, "No protocol version found, assuming 0.");
+                       protocolVersion = 0;
+               }
+
+               if (protocolVersion < 0) {
+                       logger.log(Level.WARNING, "Invalid protocol version: " + protocolVersion + "! Not parsing Sone.");
+                       return null;
+               }
+
+               /* check for valid versions. */
+               if (protocolVersion > MAX_PROTOCOL_VERSION) {
+                       logger.log(Level.WARNING, "Unknown protocol version: " + protocolVersion + "! Not parsing Sone.");
+                       return null;
+               }
+
                String soneTime = soneXml.getValue("time", null);
                if (soneTime == null) {
                        /* TODO - mark Sone as bad. */
diff --git a/src/main/java/net/pterodactylus/sone/core/UpdateChecker.java b/src/main/java/net/pterodactylus/sone/core/UpdateChecker.java
new file mode 100644 (file)
index 0000000..0839cb9
--- /dev/null
@@ -0,0 +1,233 @@
+/*
+ * Sone - UpdateChecker.java - Copyright © 2011 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.MalformedURLException;
+import java.util.Date;
+import java.util.Properties;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import net.pterodactylus.sone.main.SonePlugin;
+import net.pterodactylus.util.collection.Pair;
+import net.pterodactylus.util.io.Closer;
+import net.pterodactylus.util.logging.Logging;
+import net.pterodactylus.util.version.Version;
+import freenet.client.FetchResult;
+import freenet.keys.FreenetURI;
+import freenet.support.api.Bucket;
+
+/**
+ * Watches the official Sone homepage for new releases.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class UpdateChecker {
+
+       /** The logger. */
+       private static final Logger logger = Logging.getLogger(UpdateChecker.class);
+
+       /** The key of the Sone homepage. */
+       private static final String SONE_HOMEPAGE = "USK@nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI,DuQSUZiI~agF8c-6tjsFFGuZ8eICrzWCILB60nT8KKo,AQACAAE/sone/";
+
+       /** The current latest known edition. */
+       private static final int LATEST_EDITION = 23;
+
+       /** The Freenet interface. */
+       private final FreenetInterface freenetInterface;
+
+       /** The update listener manager. */
+       private final UpdateListenerManager updateListenerManager = new UpdateListenerManager();
+
+       /** The current URI of the homepage. */
+       private FreenetURI currentUri;
+
+       /** The current latest known version. */
+       private Version currentLatestVersion = SonePlugin.VERSION;
+
+       /** The release date of the latest version. */
+       private long latestVersionDate;
+
+       /**
+        * Creates a new update checker.
+        *
+        * @param freenetInterface
+        *            The freenet interface to use
+        */
+       public UpdateChecker(FreenetInterface freenetInterface) {
+               this.freenetInterface = freenetInterface;
+       }
+
+       //
+       // EVENT LISTENER MANAGEMENT
+       //
+
+       /**
+        * Adds the given listener to the list of registered listeners.
+        *
+        * @param updateListener
+        *            The listener to add
+        */
+       public void addUpdateListener(UpdateListener updateListener) {
+               updateListenerManager.addListener(updateListener);
+       }
+
+       /**
+        * Removes the given listener from the list of registered listeners.
+        *
+        * @param updateListener
+        *            The listener to remove
+        */
+       public void removeUpdateListener(UpdateListener updateListener) {
+               updateListenerManager.removeListener(updateListener);
+       }
+
+       //
+       // ACCESSORS
+       //
+
+       /**
+        * Returns whether a version that is later than the currently running
+        * version has been found.
+        *
+        * @return {@code true} if a new version was found
+        */
+       public boolean hasLatestVersion() {
+               return currentLatestVersion.compareTo(SonePlugin.VERSION) > 0;
+       }
+
+       /**
+        * Returns the latest version. If no new latest version has been found, the
+        * current version is returned.
+        *
+        * @return The latest known version
+        */
+       public Version getLatestVersion() {
+               return currentLatestVersion;
+       }
+
+       /**
+        * Returns the release time of the latest version. If no new latest version
+        * has been found, the returned value is undefined.
+        *
+        * @return The release time of the latest version, if a new version was
+        *         found
+        */
+       public long getLatestVersionDate() {
+               return latestVersionDate;
+       }
+
+       //
+       // ACTIONS
+       //
+
+       /**
+        * Starts the update checker.
+        */
+       public void start() {
+               try {
+                       currentUri = new FreenetURI(SONE_HOMEPAGE + LATEST_EDITION);
+               } catch (MalformedURLException mue1) {
+                       /* this can not really happen unless I screw up. */
+                       logger.log(Level.SEVERE, "Sone Homepage URI invalid!", mue1);
+               }
+               freenetInterface.registerUsk(currentUri, new FreenetInterface.Callback() {
+
+                       @Override
+                       @SuppressWarnings("synthetic-access")
+                       public void editionFound(FreenetURI uri, long edition, boolean newKnownGood, boolean newSlot) {
+                               logger.log(Level.FINEST, "Found update for %s: %d, %s, %s", new Object[] { uri, edition, newKnownGood, newSlot });
+                               if (newKnownGood || newSlot) {
+                                       Pair<FreenetURI, FetchResult> uriResult = freenetInterface.fetchUri(uri.setMetaString(new String[] { "sone.properties" }));
+                                       if (uriResult == null) {
+                                               logger.log(Level.WARNING, "Could not fetch properties of latest homepage: %s", uri);
+                                               return;
+                                       }
+                                       Bucket resultBucket = uriResult.getRight().asBucket();
+                                       try {
+                                               parseProperties(resultBucket.getInputStream());
+                                       } catch (IOException ioe1) {
+                                               logger.log(Level.WARNING, "Could not parse sone.properties of " + uri, ioe1);
+                                       } finally {
+                                               resultBucket.free();
+                                       }
+                               }
+                       }
+               });
+       }
+
+       /**
+        * Stops the update checker.
+        */
+       public void stop() {
+               freenetInterface.unregisterUsk(currentUri);
+       }
+
+       //
+       // PRIVATE ACTIONS
+       //
+
+       /**
+        * Parses the properties of the latest version and fires events, if
+        * necessary.
+        *
+        * @see UpdateListener#updateFound(Version, long)
+        * @see UpdateListenerManager#fireUpdateFound(Version, long)
+        * @param propertiesInputStream
+        *            The input stream to parse
+        * @throws IOException
+        *             if an I/O error occured
+        */
+       private void parseProperties(InputStream propertiesInputStream) throws IOException {
+               Properties properties = new Properties();
+               InputStreamReader inputStreamReader = null;
+               try {
+                       inputStreamReader = new InputStreamReader(propertiesInputStream, "UTF-8");
+                       properties.load(inputStreamReader);
+               } finally {
+                       Closer.close(inputStreamReader);
+               }
+               String versionString = properties.getProperty("CurrentVersion/Version");
+               String releaseTimeString = properties.getProperty("CurrentVersion/ReleaseTime");
+               if ((versionString == null) || (releaseTimeString == null)) {
+                       logger.log(Level.INFO, "Invalid data parsed from properties.");
+                       return;
+               }
+               Version version = Version.parse(versionString);
+               long releaseTime = 0;
+               try {
+                       releaseTime = Long.parseLong(releaseTimeString);
+               } catch (NumberFormatException nfe1) {
+                       /* ignore. */
+               }
+               if ((version == null) || (releaseTime == 0)) {
+                       logger.log(Level.INFO, "Could not parse data from properties.");
+                       return;
+               }
+               if (version.compareTo(currentLatestVersion) > 0) {
+                       currentLatestVersion = version;
+                       latestVersionDate = releaseTime;
+                       logger.log(Level.INFO, "Found new version: %s (%tc)", new Object[] { version, new Date(releaseTime) });
+                       updateListenerManager.fireUpdateFound(version, releaseTime);
+               }
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/core/UpdateListener.java b/src/main/java/net/pterodactylus/sone/core/UpdateListener.java
new file mode 100644 (file)
index 0000000..1469afa
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+ * Sone - UpdateListener.java - Copyright © 2011 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core;
+
+import java.util.EventListener;
+
+import net.pterodactylus.util.version.Version;
+
+/**
+ * Listener interface for {@link UpdateChecker} events.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface UpdateListener extends EventListener {
+
+       /**
+        * Notifies a listener that a newer version than the current version was
+        * found.
+        *
+        * @param version
+        *            The version that was found
+        * @param releaseTime
+        *            The release time of the version
+        */
+       public void updateFound(Version version, long releaseTime);
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/core/UpdateListenerManager.java b/src/main/java/net/pterodactylus/sone/core/UpdateListenerManager.java
new file mode 100644 (file)
index 0000000..3f3b21f
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * Sone - UpdateListenerManager.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.util.event.AbstractListenerManager;
+import net.pterodactylus.util.version.Version;
+
+/**
+ * Listener manager for {@link UpdateListener} events.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class UpdateListenerManager extends AbstractListenerManager<Void, UpdateListener> {
+
+       /**
+        * Creates a new update listener manager.
+        */
+       public UpdateListenerManager() {
+               super(null);
+       }
+
+       //
+       // ACTIONS
+       //
+
+       /**
+        * Notifies all listeners that a new version has been found.
+        *
+        * @param version
+        *            The new version
+        * @param releaseTime
+        *            The release time of the new version
+        */
+       void fireUpdateFound(Version version, long releaseTime) {
+               for (UpdateListener updateListener : getListeners()) {
+                       updateListener.updateFound(version, releaseTime);
+               }
+       }
+
+}
index b8c86cc..219f989 100644 (file)
@@ -78,7 +78,7 @@ public class SonePlugin implements FredPlugin, FredPluginL10n, FredPluginBaseL10
        }
 
        /** The version. */
-       public static final Version VERSION = new Version(0, 3, 6, 2);
+       public static final Version VERSION = new Version(0, 3, 6, 5);
 
        /** The logger. */
        private static final Logger logger = Logging.getLogger(SonePlugin.class);
@@ -166,28 +166,28 @@ public class SonePlugin implements FredPlugin, FredPluginL10n, FredPluginBaseL10
                        }
                }
 
-               /* create freenet interface. */
-               FreenetInterface freenetInterface = new FreenetInterface(pluginRespirator.getNode());
+               boolean startupFailed = true;
+               try {
+                       /* create freenet interface. */
+                       FreenetInterface freenetInterface = new FreenetInterface(pluginRespirator.getNode());
 
-               /* create web of trust connector. */
-               PluginConnector pluginConnector = new PluginConnector(pluginRespirator);
-               WebOfTrustConnector webOfTrustConnector = new WebOfTrustConnector(pluginConnector);
-               identityManager = new IdentityManager(webOfTrustConnector);
-               identityManager.setContext("Sone");
+                       /* create web of trust connector. */
+                       PluginConnector pluginConnector = new PluginConnector(pluginRespirator);
+                       WebOfTrustConnector webOfTrustConnector = new WebOfTrustConnector(pluginConnector);
+                       identityManager = new IdentityManager(webOfTrustConnector);
+                       identityManager.setContext("Sone");
 
-               /* create core. */
-               core = new Core(oldConfiguration, freenetInterface, identityManager);
+                       /* create core. */
+                       core = new Core(oldConfiguration, freenetInterface, identityManager);
 
-               /* create the web interface. */
-               webInterface = new WebInterface(this);
-               core.addCoreListener(webInterface);
+                       /* create the web interface. */
+                       webInterface = new WebInterface(this);
+                       core.addCoreListener(webInterface);
 
-               /* create the identity manager. */
-               identityManager.addIdentityListener(core);
+                       /* create the identity manager. */
+                       identityManager.addIdentityListener(core);
 
-               /* start core! */
-               boolean startupFailed = true;
-               try {
+                       /* start core! */
                        core.start();
                        if ((newConfiguration != null) && (oldConfiguration != newConfiguration)) {
                                logger.log(Level.INFO, "Setting configuration to file-based configuration.");
index 495d805..95dc614 100644 (file)
@@ -24,6 +24,7 @@ import net.pterodactylus.sone.core.Core;
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.text.FreenetLinkParser;
+import net.pterodactylus.sone.text.FreenetLinkParserContext;
 import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.ReflectionAccessor;
 import net.pterodactylus.util.template.TemplateFactory;
@@ -79,7 +80,7 @@ public class PostAccessor extends ReflectionAccessor {
                                return null;
                        }
                        try {
-                               return linkParser.parse(new StringReader(text));
+                               return linkParser.parse(new FreenetLinkParserContext(post.getSone()), new StringReader(text));
                        } catch (IOException ioe1) {
                                /* ignore. */
                        }
index 1ec4e04..db6c528 100644 (file)
@@ -24,6 +24,7 @@ import net.pterodactylus.sone.core.Core;
 import net.pterodactylus.sone.data.Reply;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.text.FreenetLinkParser;
+import net.pterodactylus.sone.text.FreenetLinkParserContext;
 import net.pterodactylus.util.template.Accessor;
 import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.ReflectionAccessor;
@@ -72,7 +73,7 @@ public class ReplyAccessor extends ReflectionAccessor {
                } else if (member.equals("text")) {
                        String text = reply.getText();
                        try {
-                               return linkParser.parse(new StringReader(text));
+                               return linkParser.parse(new FreenetLinkParserContext(reply.getSone()), new StringReader(text));
                        } catch (IOException ioe1) {
                                /* ignore. */
                        }
index 1d7c6b7..63f68b8 100644 (file)
@@ -21,6 +21,7 @@ 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;
@@ -28,13 +29,14 @@ import java.util.regex.Pattern;
 
 import net.pterodactylus.util.logging.Logging;
 import net.pterodactylus.util.template.TemplateFactory;
+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 {
+public class FreenetLinkParser implements Parser<FreenetLinkParserContext> {
 
        /** The logger. */
        private static final Logger logger = Logging.getLogger(FreenetLinkParser.class);
@@ -50,46 +52,22 @@ public class FreenetLinkParser implements Parser {
        private enum LinkType {
 
                /** Link is a KSK. */
-               KSK(true),
+               KSK,
 
                /** Link is a CHK. */
-               CHK(true),
+               CHK,
 
                /** Link is an SSK. */
-               SSK(true),
+               SSK,
 
                /** Link is a USK. */
-               USK(true),
+               USK,
 
                /** Link is HTTP. */
-               HTTP(false),
+               HTTP,
 
                /** Link is HTTPS. */
-               HTTPS(false);
-
-               /** Whether this link type links to freenet. */
-               private final boolean anonymous;
-
-               /**
-                * Creates a new link type.
-                *
-                * @param anonymous
-                *            {@code true} if this link type links to freenet,
-                *            {@code false} otherwise
-                */
-               private LinkType(boolean anonymous) {
-                       this.anonymous = anonymous;
-               }
-
-               /**
-                * Returns whether this link type links anonymously to within freenet.
-                *
-                * @return {@code true} if this link type links to within freenet,
-                *         {@code false} otherwise
-                */
-               public boolean isAnonymous() {
-                       return anonymous;
-               }
+               HTTPS;
 
        }
 
@@ -114,8 +92,7 @@ public class FreenetLinkParser implements Parser {
         * {@inheritDoc}
         */
        @Override
-       @SuppressWarnings("null")
-       public Part parse(Reader source) throws IOException {
+       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;
@@ -170,8 +147,37 @@ public class FreenetLinkParser implements Parser {
                                        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.CHK) || (linkType == LinkType.SSK) || (linkType == LinkType.USK)) && (link.length() > 98) && (link.charAt(47) == ',') && (link.charAt(91) == ',') && (link.charAt(99) == '/')) {
-                                               name = link.substring(0, 47) + "…" + link.substring(99);
+                                       if (linkType == LinkType.KSK) {
+                                               name = link.substring(4);
+                                       } else if ((linkType == LinkType.CHK) || (linkType == LinkType.SSK) || (linkType == LinkType.USK)) {
+                                               if (name.indexOf('/') > -1) {
+                                                       if (!name.endsWith("/")) {
+                                                               name = name.substring(name.lastIndexOf('/') + 1);
+                                                       } else {
+                                                               if (name.indexOf('/') != name.lastIndexOf('/')) {
+                                                                       name = name.substring(name.lastIndexOf('/', name.lastIndexOf('/') - 1));
+                                                               } else {
+                                                                       /* shorten to 5 chars. */
+                                                                       name = name.substring(4, Math.min(9, name.length()));
+                                                               }
+                                                       }
+                                               }
+                                               if (name.indexOf('?') > -1) {
+                                                       name = name.substring(0, name.indexOf('?'));
+                                               }
+                                               boolean fromPostingSone = false;
+                                               if ((linkType == LinkType.SSK) || (linkType == LinkType.USK)) {
+                                                       try {
+                                                               new FreenetURI(link);
+                                                               fromPostingSone = link.substring(4, Math.min(link.length(), 47)).equals(context.getPostingSone().getId());
+                                                               parts.add(fromPostingSone ? createTrustedFreenetLinkPart(link, name) : createFreenetLinkPart(link, name));
+                                                       } catch (MalformedURLException mue1) {
+                                                               /* it’s not a valid link. */
+                                                               parts.add(createPlainTextPart(link));
+                                                       }
+                                               } else {
+                                                       parts.add(fromPostingSone ? createTrustedFreenetLinkPart(link, name) : createFreenetLinkPart(link, name));
+                                               }
                                        } else if ((linkType == LinkType.HTTP) || (linkType == LinkType.HTTPS)) {
                                                name = link.substring(linkType == LinkType.HTTP ? 7 : 8);
                                                int firstSlash = name.indexOf('/');
@@ -185,9 +191,12 @@ public class FreenetLinkParser implements Parser {
                                                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));
                                        }
-                                       parts.add(createLinkPart(linkType.isAnonymous(), link, name));
                                        line = line.substring(nextSpace);
                                } else {
                                        parts.add(createPlainTextPart(line.substring(0, next + 4)));
@@ -214,19 +223,45 @@ public class FreenetLinkParser implements Parser {
        }
 
        /**
-        * Creates a new link part based on a template.
+        * 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(templateFactory.createTemplate(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(templateFactory.createTemplate(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 anonymous
-        *            {@code true} if this link points to within freenet,
-        *            {@code false} if it points to the internet
         * @param link
         *            The target of the link
         * @param name
         *            The name of the link
         * @return The part that displays the link
         */
-       private Part createLinkPart(boolean anonymous, String link, String name) {
-               return new TemplatePart(templateFactory.createTemplate(new StringReader("<a <%if !anonymous>class=\"internet\" <%/if>href=\"<% link|html>\"><% name|html></a>"))).set("anonymous", anonymous).set("link", "/" + link).set("name", name);
+       private Part createTrustedFreenetLinkPart(String link, String name) {
+               return new TemplatePart(templateFactory.createTemplate(new StringReader("<a class=\"freenet-trusted\" href=\"/<% link|html>\" title=\"<% link|html>\"><% name|html></a>"))).set("link", link).set("name", name);
        }
 
 }
diff --git a/src/main/java/net/pterodactylus/sone/text/FreenetLinkParserContext.java b/src/main/java/net/pterodactylus/sone/text/FreenetLinkParserContext.java
new file mode 100644 (file)
index 0000000..e524d91
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+ * Sone - FreenetLinkParserContext.java - Copyright © 2011 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.text;
+
+import net.pterodactylus.sone.data.Sone;
+
+/**
+ * {@link ParserContext} implementation for the {@link FreenetLinkParser}. It
+ * stores the {@link Sone} that provided the parsed text so that certain links
+ * can be marked in a different way.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class FreenetLinkParserContext implements ParserContext {
+
+       /** The posting Sone. */
+       private final Sone postingSone;
+
+       /**
+        * Creates a new link parser context.
+        *
+        * @param postingSone
+        *            The posting Sone
+        */
+       public FreenetLinkParserContext(Sone postingSone) {
+               this.postingSone = postingSone;
+       }
+
+       /**
+        * Returns the Sone that provided the text that is being parsed.
+        *
+        * @return The posting Sone
+        */
+       public Sone getPostingSone() {
+               return postingSone;
+       }
+
+}
index ccec36a..e43ed47 100644 (file)
@@ -24,19 +24,23 @@ import java.io.Reader;
  * Interface for parsers that can create {@link Part}s from a text source
  * (usually a {@link Reader}).
  *
+ * @param <C>
+ *            The type of the parser context
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
-public interface Parser {
+public interface Parser<C extends ParserContext> {
 
        /**
         * Create a {@link Part} from the given text source.
         *
+        * @param context
+        *            The parser context
         * @param source
         *            The text source
         * @return The parsed part
         * @throws IOException
         *             if an I/O error occurs
         */
-       public Part parse(Reader source) throws IOException;
+       public Part parse(C context, Reader source) throws IOException;
 
 }
diff --git a/src/main/java/net/pterodactylus/sone/text/ParserContext.java b/src/main/java/net/pterodactylus/sone/text/ParserContext.java
new file mode 100644 (file)
index 0000000..7f38ad8
--- /dev/null
@@ -0,0 +1,15 @@
+
+package net.pterodactylus.sone.text;
+
+/**
+ * Context for the {@link Parser}. This interface needs to be implemented by
+ * {@link Parser}s that need to provide more information than just the text to
+ * parse to {@link Parser#parse(ParserContext, java.io.Reader)}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface ParserContext {
+
+       /* nothing to see. */
+
+}
index 23ca247..3d4cb80 100644 (file)
@@ -21,6 +21,7 @@ import java.util.Arrays;
 import java.util.Collection;
 
 import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.main.SonePlugin;
 import net.pterodactylus.sone.web.page.Page;
 import net.pterodactylus.sone.web.page.TemplatePage;
 import net.pterodactylus.util.template.Template;
@@ -188,6 +189,10 @@ public class SoneTemplatePage extends TemplatePage {
                super.processTemplate(request, template);
                template.set("currentSone", getCurrentSone(request.getToadletContext(), false));
                template.set("request", request);
+               template.set("currentVersion", SonePlugin.VERSION);
+               template.set("hasLatestVersion", webInterface.getCore().getUpdateChecker().hasLatestVersion());
+               template.set("latestVersion", webInterface.getCore().getUpdateChecker().getLatestVersion());
+               template.set("latestVersionTime", webInterface.getCore().getUpdateChecker().getLatestVersionDate());
        }
 
        /**
index d3ec1ee..6085ff8 100644 (file)
@@ -88,6 +88,7 @@ import net.pterodactylus.util.template.TemplateFactory;
 import net.pterodactylus.util.template.TemplateProvider;
 import net.pterodactylus.util.template.XmlFilter;
 import net.pterodactylus.util.thread.Ticker;
+import net.pterodactylus.util.version.Version;
 import freenet.clients.http.SessionManager;
 import freenet.clients.http.SessionManager.Session;
 import freenet.clients.http.ToadletContainer;
@@ -141,6 +142,9 @@ public class WebInterface implements CoreListener {
        /** The “Sone locked” notification. */
        private final ListNotification<Sone> lockedSonesNotification;
 
+       /** The “new version” notification. */
+       private final TemplateNotification newVersionNotification;
+
        /**
         * Creates a new web interface.
         *
@@ -189,6 +193,9 @@ public class WebInterface implements CoreListener {
 
                Template lockedSonesTemplate = templateFactory.createTemplate(createReader("/templates/notify/lockedSonesNotification.html"));
                lockedSonesNotification = new ListNotification<Sone>("sones-locked-notification", "sones", lockedSonesTemplate);
+
+               Template newVersionTemplate = templateFactory.createTemplate(createReader("/templates/notify/newVersionNotification.html"));
+               newVersionNotification = new TemplateNotification("new-version-notification", newVersionTemplate);
        }
 
        //
@@ -701,6 +708,16 @@ public class WebInterface implements CoreListener {
        }
 
        /**
+        * {@inheritDoc}
+        */
+       @Override
+       public void updateFound(Version version, long releaseTime) {
+               newVersionNotification.set("version", version);
+               newVersionNotification.set("releaseTime", releaseTime);
+               notificationManager.addNotification(newVersionNotification);
+       }
+
+       /**
         * Template provider implementation that uses
         * {@link WebInterface#createReader(String)} to load templates for
         * inclusion.
index f8ce026..740cf45 100644 (file)
@@ -175,12 +175,15 @@ WebInterface.SelectBox.Choose=Choose…
 WebInterface.SelectBox.Yes=Yes
 WebInterface.SelectBox.No=No
 WebInterface.ClickToShow.Replies=Click here to show hidden replies.
+WebInterface.VersionInformation.CurrentVersion=Current Version:
+WebInterface.VersionInformation.LatestVersion=Latest Version:
 
 Notification.ClickHereToRead=Click here to read the full text of the notification.
 Notification.FirstStart.Text=This seems to be the first time you start Sone. To start, create a new Sone from a web of trust identity and start following other Sones.
 Notification.Startup.Text=Sone is currently starting up. It may take a while to retrieve all identities and Sones from the web of trust. If you are missing some elements, please be patient, they will probably reappear very soon.
 Notification.ConfigNotRead.Text=The configuration file “sone.properties” could not be read, probably because it was not saved correctly. This can happen on versions prior to Sone 0.3.3 and there is nothing you can do about it.
 Notification.Button.Dismiss=Dismiss
+Notification.NewSone.ShortText=New Sones have been discovered:
 Notification.NewSone.Text=New Sones have been discovered:
 Notification.NewPost.ShortText=New posts have been discovered.
 Notification.NewPost.Text=New posts have been discovered by the following Sones:
@@ -190,3 +193,4 @@ Notification.SoneIsBeingRescued.Text=The following Sones are currently being res
 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=A new version of the Sone plugin was found: Version {version}.
index 00d9e36..dfd6b9a 100644 (file)
@@ -49,10 +49,30 @@ textarea {
        color: rgb(255, 172, 0);
 }
 
+#sone a.link {
+       cursor: pointer;
+}
+
 #sone a.internet {
        color: rgb(255, 0, 0);
 }
 
+#sone a.internet:before {
+       content: '⚠ ';
+}
+
+#sone a.freenet:before {
+       content: '» ';
+}
+
+#sone a.freenet-trusted {
+       color: rgb(0, 128, 0);
+}
+
+#sone a.freenet-trusted:before {
+       content: '★ ';
+}
+
 #sone a img {
        border: none;
 }
@@ -424,6 +444,10 @@ textarea {
        color: #888;
 }
 
+#sone #tail #version-information {
+       margin-top: 1em;
+}
+
 #sone #add-sone textarea, #sone #create-sone textarea, #sone #load-sone textarea, #sone #edit-profile textarea {
        height: 1.5em;
 }
index 180bcf2..dd2e25e 100644 (file)
@@ -7,6 +7,13 @@
                                <img src="images/flattr-badge-large.png" alt="Flattr Sone" title="Flattr Sone" />
                        </a>
                </div>
+
+               <div id="version-information">
+                       <div class="current-version"><%= WebInterface.VersionInformation.CurrentVersion|l10n|html> <b><% currentVersion|html></b></div>
+                       <%if hasLatestVersion>
+                               <div class="latest-version"><%= WebInterface.VersionInformation.LatestVersion|l10n|html> <b><% latestVersion|html></b></div>
+                       <%/if>
+               </div>
        </div>
 
 </div>
index a2c6c09..1f12d90 100644 (file)
@@ -2,6 +2,7 @@
 <sone>
 
        <time><% currentSone.time></time>
+       <protocol-version>0</protocol-version>
 
        <client>
                <name>Sone</name>
index f17a1ba..94f7a1f 100644 (file)
@@ -1,6 +1,6 @@
 <div class="short-text">
        <%= Notification.NewPost.ShortText|l10n|html>
-       <a href="javascript:showNotificationDetails('<%notification.id|html>'); return false;"><%= Notification.ClickHereToRead|l10n|html></a>
+       <a class="link" onclick="showNotificationDetails('<%notification.id|html>'); return false;"><%= Notification.ClickHereToRead|l10n|html></a>
 </div>
 <div class="text hidden">
        <%= Notification.NewPost.Text|l10n|html>
index 0cf203b..d6d0670 100644 (file)
@@ -1,6 +1,6 @@
 <div class="short-text">
        <%= Notification.NewReply.ShortText|l10n|html>
-       <a href="javascript:showNotificationDetails('<%notification.id|html>'); return false;"><%= Notification.ClickHereToRead|l10n|html></a>
+       <a class="link" onclick="showNotificationDetails('<%notification.id|html>'); return false;"><%= Notification.ClickHereToRead|l10n|html></a>
 </div>
 <div class="text hidden">
        <%= Notification.NewReply.Text|l10n|html>
index a13d686..8389f7a 100644 (file)
@@ -1,4 +1,8 @@
-<div class="text">
+<div class="short-text">
+       <%= Notification.NewSone.ShortText|l10n|html>
+       <a class="link" onclick="showNotificationDetails('<%notification.id|html>'); return false;"><%= Notification.ClickHereToRead|l10n|html></a>
+</div>
+<div class="text hidden">
        <%= Notification.NewSone.Text|l10n|html>
        <%foreach sones sone>
                <a href="viewSone.html?sone=<% sone.id|html>" title="<% sone.requestUri|html>"><% sone.niceName|html></a><%notlast>,<%/notlast><%last>.<%/last>
diff --git a/src/main/resources/templates/notify/newVersionNotification.html b/src/main/resources/templates/notify/newVersionNotification.html
new file mode 100644 (file)
index 0000000..23a5855
--- /dev/null
@@ -0,0 +1 @@
+<div class="text"><%= Notification.NewVersion.Text|l10n|html|replace needle="{version}" replacementKey=version></div>