Merge branch 'next' into image-management
authorDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Tue, 22 Mar 2011 18:46:01 +0000 (19:46 +0100)
committerDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Tue, 22 Mar 2011 18:46:01 +0000 (19:46 +0100)
Conflicts:
src/main/java/net/pterodactylus/sone/core/Core.java
src/main/java/net/pterodactylus/sone/data/Sone.java
src/main/java/net/pterodactylus/sone/web/WebInterface.java
src/main/resources/i18n/sone.en.properties

136 files changed:
pom.xml
src/main/java/net/pterodactylus/sone/core/Core.java
src/main/java/net/pterodactylus/sone/core/CoreListener.java
src/main/java/net/pterodactylus/sone/core/CoreListenerManager.java
src/main/java/net/pterodactylus/sone/core/FreenetInterface.java
src/main/java/net/pterodactylus/sone/core/Options.java
src/main/java/net/pterodactylus/sone/core/SoneDownloader.java
src/main/java/net/pterodactylus/sone/core/SoneInserter.java
src/main/java/net/pterodactylus/sone/core/UpdateChecker.java
src/main/java/net/pterodactylus/sone/core/UpdateListener.java
src/main/java/net/pterodactylus/sone/core/UpdateListenerManager.java
src/main/java/net/pterodactylus/sone/data/Profile.java
src/main/java/net/pterodactylus/sone/data/Sone.java
src/main/java/net/pterodactylus/sone/freenet/L10nFilter.java
src/main/java/net/pterodactylus/sone/freenet/plugin/ConnectorListener.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/freenet/plugin/ConnectorListenerManager.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/freenet/plugin/PluginConnector.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/freenet/plugin/PluginException.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/freenet/wot/ConnectorListener.java [deleted file]
src/main/java/net/pterodactylus/sone/freenet/wot/ConnectorListenerManager.java [deleted file]
src/main/java/net/pterodactylus/sone/freenet/wot/DefaultIdentity.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/freenet/wot/DefaultOwnIdentity.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/freenet/wot/Identity.java
src/main/java/net/pterodactylus/sone/freenet/wot/IdentityListener.java
src/main/java/net/pterodactylus/sone/freenet/wot/IdentityListenerManager.java
src/main/java/net/pterodactylus/sone/freenet/wot/IdentityManager.java
src/main/java/net/pterodactylus/sone/freenet/wot/OwnIdentity.java
src/main/java/net/pterodactylus/sone/freenet/wot/PluginConnector.java [deleted file]
src/main/java/net/pterodactylus/sone/freenet/wot/PluginException.java [deleted file]
src/main/java/net/pterodactylus/sone/freenet/wot/Trust.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/freenet/wot/WebOfTrustConnector.java
src/main/java/net/pterodactylus/sone/freenet/wot/WebOfTrustException.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/template/AlbumAccessor.java
src/main/java/net/pterodactylus/sone/template/CollectionAccessor.java
src/main/java/net/pterodactylus/sone/template/CssClassNameFilter.java
src/main/java/net/pterodactylus/sone/template/GetPagePlugin.java
src/main/java/net/pterodactylus/sone/template/IdentityAccessor.java
src/main/java/net/pterodactylus/sone/template/JavascriptFilter.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/template/NotificationManagerAccessor.java
src/main/java/net/pterodactylus/sone/template/ParserFilter.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/template/PostAccessor.java
src/main/java/net/pterodactylus/sone/template/ReplyAccessor.java
src/main/java/net/pterodactylus/sone/template/RequestChangeFilter.java
src/main/java/net/pterodactylus/sone/template/SoneAccessor.java
src/main/java/net/pterodactylus/sone/template/SubstringFilter.java
src/main/java/net/pterodactylus/sone/template/TrustAccessor.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/template/UnknownDateFilter.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/text/FreenetLinkParser.java
src/main/java/net/pterodactylus/sone/text/TemplatePart.java
src/main/java/net/pterodactylus/sone/web/AboutPage.java
src/main/java/net/pterodactylus/sone/web/BookmarkPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/BookmarksPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/CreateAlbumPage.java
src/main/java/net/pterodactylus/sone/web/CreatePostPage.java
src/main/java/net/pterodactylus/sone/web/CreateReplyPage.java
src/main/java/net/pterodactylus/sone/web/CreateSonePage.java
src/main/java/net/pterodactylus/sone/web/DeletePostPage.java
src/main/java/net/pterodactylus/sone/web/DeleteProfileFieldPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/DeleteReplyPage.java
src/main/java/net/pterodactylus/sone/web/DeleteSonePage.java
src/main/java/net/pterodactylus/sone/web/DismissNotificationPage.java
src/main/java/net/pterodactylus/sone/web/DistrustPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/EditProfileFieldPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/EditProfilePage.java
src/main/java/net/pterodactylus/sone/web/FollowSonePage.java
src/main/java/net/pterodactylus/sone/web/ImageBrowserPage.java
src/main/java/net/pterodactylus/sone/web/IndexPage.java
src/main/java/net/pterodactylus/sone/web/KnownSonesPage.java
src/main/java/net/pterodactylus/sone/web/LikePage.java
src/main/java/net/pterodactylus/sone/web/LockSonePage.java
src/main/java/net/pterodactylus/sone/web/LoginPage.java
src/main/java/net/pterodactylus/sone/web/LogoutPage.java
src/main/java/net/pterodactylus/sone/web/MarkAsKnownPage.java [new file with mode: 0644]
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/TrustPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/UnbookmarkPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/UnfollowSonePage.java
src/main/java/net/pterodactylus/sone/web/UnlikePage.java
src/main/java/net/pterodactylus/sone/web/UnlockSonePage.java
src/main/java/net/pterodactylus/sone/web/UntrustPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/ViewPostPage.java
src/main/java/net/pterodactylus/sone/web/ViewSonePage.java
src/main/java/net/pterodactylus/sone/web/WebInterface.java
src/main/java/net/pterodactylus/sone/web/ajax/BookmarkAjaxPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/ajax/CreatePostAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/CreateReplyAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/DeletePostAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/DeleteProfileFieldAjaxPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/ajax/DeleteReplyAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/DismissNotificationAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/DistrustAjaxPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/ajax/EditProfileFieldAjaxPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/ajax/GetPostAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/GetReplyAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/GetStatusAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/GetTranslationPage.java
src/main/java/net/pterodactylus/sone/web/ajax/JsonPage.java
src/main/java/net/pterodactylus/sone/web/ajax/LockSoneAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/MarkAsKnownAjaxPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/ajax/MarkPostAsKnownPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/ajax/MarkReplyAsKnownPage.java [deleted file]
src/main/java/net/pterodactylus/sone/web/ajax/MoveProfileFieldAjaxPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/ajax/TrustAjaxPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/ajax/UnbookmarkAjaxPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/ajax/UnlockSoneAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/UntrustAjaxPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/page/TemplatePage.java
src/main/resources/i18n/sone.en.properties
src/main/resources/static/css/sone.css
src/main/resources/static/images/icon-activity.png [new file with mode: 0644]
src/main/resources/static/javascript/jquery.url.js [new file with mode: 0644]
src/main/resources/static/javascript/sone.js
src/main/resources/templates/bookmarks.html [new file with mode: 0644]
src/main/resources/templates/deleteProfileField.html [new file with mode: 0644]
src/main/resources/templates/editProfile.html
src/main/resources/templates/editProfileField.html [new file with mode: 0644]
src/main/resources/templates/include/head.html
src/main/resources/templates/include/tail.html
src/main/resources/templates/include/updateStatus.html
src/main/resources/templates/include/viewPost.html
src/main/resources/templates/include/viewReply.html
src/main/resources/templates/include/viewSone.html
src/main/resources/templates/index.html
src/main/resources/templates/insert/sone.xml
src/main/resources/templates/invalid.html [new file with mode: 0644]
src/main/resources/templates/knownSones.html
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
src/main/resources/templates/options.html
src/main/resources/templates/viewPost.html
src/main/resources/templates/viewSone.html

diff --git a/pom.xml b/pom.xml
index e9ff09c..229f2ef 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-5</version>
+       <version>0.5.1</version>
        <dependencies>
                <dependency>
                        <groupId>net.pterodactylus</groupId>
                        <artifactId>utils</artifactId>
-                       <version>0.7.7</version>
+                       <version>0.9.1</version>
                </dependency>
                <dependency>
                        <groupId>junit</groupId>
@@ -18,7 +18,7 @@
                <dependency>
                        <groupId>org.freenetproject</groupId>
                        <artifactId>fred</artifactId>
-                       <version>0.7.5.1311</version>
+                       <version>0.7.5.1336</version>
                        <scope>provided</scope>
                </dependency>
                <dependency>
index 6813493..d61211d 100644 (file)
@@ -36,17 +36,21 @@ import net.pterodactylus.sone.data.Client;
 import net.pterodactylus.sone.data.Image;
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Profile;
+import net.pterodactylus.sone.data.Profile.Field;
 import net.pterodactylus.sone.data.Reply;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.freenet.wot.Identity;
 import net.pterodactylus.sone.freenet.wot.IdentityListener;
 import net.pterodactylus.sone.freenet.wot.IdentityManager;
 import net.pterodactylus.sone.freenet.wot.OwnIdentity;
+import net.pterodactylus.sone.freenet.wot.Trust;
+import net.pterodactylus.sone.freenet.wot.WebOfTrustException;
 import net.pterodactylus.sone.main.SonePlugin;
 import net.pterodactylus.util.config.Configuration;
 import net.pterodactylus.util.config.ConfigurationException;
 import net.pterodactylus.util.logging.Logging;
 import net.pterodactylus.util.number.Numbers;
+import net.pterodactylus.util.validation.Validation;
 import net.pterodactylus.util.version.Version;
 import freenet.keys.FreenetURI;
 
@@ -83,6 +87,9 @@ public class Core implements IdentityListener, UpdateListener {
        /** The options. */
        private final Options options = new Options();
 
+       /** The preferences. */
+       private final Preferences preferences = new Preferences(options);
+
        /** The core listener manager. */
        private final CoreListenerManager coreListenerManager = new CoreListenerManager(this);
 
@@ -153,6 +160,13 @@ public class Core implements IdentityListener, UpdateListener {
        /** All known replies. */
        private Set<String> knownReplies = new HashSet<String>();
 
+       /** All bookmarked posts. */
+       /* synchronize access on itself. */
+       private Set<String> bookmarkedPosts = new HashSet<String>();
+
+       /** Trusted identities, sorted by own identities. */
+       private Map<OwnIdentity, Set<Identity>> trustedIdentities = Collections.synchronizedMap(new HashMap<OwnIdentity, Set<Identity>>());
+
        /** All known albums. */
        private Map<String, Album> albums = new HashMap<String, Album>();
 
@@ -222,18 +236,8 @@ public class Core implements IdentityListener, UpdateListener {
         *
         * @return The options of the core
         */
-       public Options getOptions() {
-               return options;
-       }
-
-       /**
-        * Returns whether the “Sone rescue mode” is currently activated.
-        *
-        * @return {@code true} if the “Sone rescue mode” is currently activated,
-        *         {@code false} if it is not
-        */
-       public boolean isSoneRescueMode() {
-               return options.getBooleanOption("SoneRescueMode").get();
+       public Preferences getPreferences() {
+               return preferences;
        }
 
        /**
@@ -416,6 +420,7 @@ public class Core implements IdentityListener, UpdateListener {
                        if ((sone == null) && create) {
                                sone = new Sone(id);
                                localSones.put(id, sone);
+                               setSoneStatus(sone, SoneStatus.unknown);
                        }
                        return sone;
                }
@@ -459,6 +464,7 @@ public class Core implements IdentityListener, UpdateListener {
                        if ((sone == null) && create) {
                                sone = new Sone(id);
                                remoteSones.put(id, sone);
+                               setSoneStatus(sone, SoneStatus.unknown);
                        }
                        return sone;
                }
@@ -493,22 +499,15 @@ public class Core implements IdentityListener, UpdateListener {
        }
 
        /**
-        * Returns whether the given Sone is a new Sone. After this check, the Sone
-        * is marked as known, i.e. a second call with the same parameters will
-        * always yield {@code false}.
+        * Returns whether the Sone with the given ID is a new Sone.
         *
-        * @param sone
-        *            The sone to check for
+        * @param soneId
+        *            The ID of the sone to check for
         * @return {@code true} if the given Sone is new, false otherwise
         */
-       public boolean isNewSone(Sone sone) {
+       public boolean isNewSone(String soneId) {
                synchronized (newSones) {
-                       boolean isNew = !knownSones.contains(sone.getId()) && newSones.remove(sone.getId());
-                       knownSones.add(sone.getId());
-                       if (isNew) {
-                               coreListenerManager.fireMarkSoneKnown(sone);
-                       }
-                       return isNew;
+                       return !knownSones.contains(soneId) && newSones.contains(soneId);
                }
        }
 
@@ -525,6 +524,19 @@ public class Core implements IdentityListener, UpdateListener {
        }
 
        /**
+        * Returns whether the target Sone is trusted by the origin Sone.
+        *
+        * @param origin
+        *            The origin Sone
+        * @param target
+        *            The target Sone
+        * @return {@code true} if the target Sone is trusted by the origin Sone
+        */
+       public boolean isSoneTrusted(Sone origin, Sone target) {
+               return trustedIdentities.containsKey(origin) && trustedIdentities.get(origin.getIdentity()).contains(target);
+       }
+
+       /**
         * Returns the post with the given ID.
         *
         * @param postId
@@ -557,8 +569,7 @@ public class Core implements IdentityListener, UpdateListener {
        }
 
        /**
-        * Returns whether the given post ID is new. After this method returns it is
-        * marked a known post ID.
+        * Returns whether the given post ID is new.
         *
         * @param postId
         *            The post ID
@@ -566,32 +577,8 @@ public class Core implements IdentityListener, UpdateListener {
         *         otherwise
         */
        public boolean isNewPost(String postId) {
-               return isNewPost(postId, true);
-       }
-
-       /**
-        * Returns whether the given post ID is new. If {@code markAsKnown} is
-        * {@code true} then after this method returns the post ID is marked a known
-        * post ID.
-        *
-        * @param postId
-        *            The post ID
-        * @param markAsKnown
-        *            {@code true} to mark the post ID as known, {@code false} to
-        *            not to mark it as known
-        * @return {@code true} if the post is considered to be new, {@code false}
-        *         otherwise
-        */
-       public boolean isNewPost(String postId, boolean markAsKnown) {
                synchronized (newPosts) {
-                       boolean isNew = !knownPosts.contains(postId) && newPosts.contains(postId);
-                       if (markAsKnown) {
-                               Post post = getPost(postId, false);
-                               if (post != null) {
-                                       markPostKnown(post);
-                               }
-                       }
-                       return isNew;
+                       return !knownPosts.contains(postId) && newPosts.contains(postId);
                }
        }
 
@@ -660,30 +647,8 @@ public class Core implements IdentityListener, UpdateListener {
         *         otherwise
         */
        public boolean isNewReply(String replyId) {
-               return isNewReply(replyId, true);
-       }
-
-       /**
-        * Returns whether the reply with the given ID is new.
-        *
-        * @param replyId
-        *            The ID of the reply to check
-        * @param markAsKnown
-        *            {@code true} to mark the reply as known, {@code false} to not
-        *            to mark it as known
-        * @return {@code true} if the reply is considered to be new, {@code false}
-        *         otherwise
-        */
-       public boolean isNewReply(String replyId, boolean markAsKnown) {
                synchronized (newReplies) {
-                       boolean isNew = !knownReplies.contains(replyId) && newReplies.contains(replyId);
-                       if (markAsKnown) {
-                               Reply reply = getReply(replyId, false);
-                               if (reply != null) {
-                                       markReplyKnown(reply);
-                               }
-                       }
-                       return isNew;
+                       return !knownReplies.contains(replyId) && newReplies.contains(replyId);
                }
        }
 
@@ -722,6 +687,51 @@ public class Core implements IdentityListener, UpdateListener {
        }
 
        /**
+        * Returns whether the given post is bookmarked.
+        *
+        * @param post
+        *            The post to check
+        * @return {@code true} if the given post is bookmarked, {@code false}
+        *         otherwise
+        */
+       public boolean isBookmarked(Post post) {
+               return isPostBookmarked(post.getId());
+       }
+
+       /**
+        * Returns whether the post with the given ID is bookmarked.
+        *
+        * @param id
+        *            The ID of the post to check
+        * @return {@code true} if the post with the given ID is bookmarked,
+        *         {@code false} otherwise
+        */
+       public boolean isPostBookmarked(String id) {
+               synchronized (bookmarkedPosts) {
+                       return bookmarkedPosts.contains(id);
+               }
+       }
+
+       /**
+        * Returns all currently known bookmarked posts.
+        *
+        * @return All bookmarked posts
+        */
+       public Set<Post> getBookmarkedPosts() {
+               Set<Post> posts = new HashSet<Post>();
+               synchronized (bookmarkedPosts) {
+                       for (String bookmarkedPostId : bookmarkedPosts) {
+                               Post post = getPost(bookmarkedPostId, false);
+                               if (post != null) {
+                                       posts.add(post);
+                               }
+                       }
+               }
+               return posts;
+       }
+
+       /**
+        *      
         * Returns the album with the given ID, creating a new album if no album
         * with the given ID can be found.
         *
@@ -876,7 +886,7 @@ public class Core implements IdentityListener, UpdateListener {
                        soneInserters.put(sone, soneInserter);
                        setSoneStatus(sone, SoneStatus.idle);
                        loadSone(sone);
-                       if (!isSoneRescueMode()) {
+                       if (!preferences.isSoneRescueMode()) {
                                soneInserter.start();
                        }
                        new Thread(new Runnable() {
@@ -884,7 +894,7 @@ public class Core implements IdentityListener, UpdateListener {
                                @Override
                                @SuppressWarnings("synthetic-access")
                                public void run() {
-                                       if (!isSoneRescueMode()) {
+                                       if (!preferences.isSoneRescueMode()) {
                                                soneDownloader.fetchSone(sone);
                                                return;
                                        }
@@ -892,7 +902,7 @@ public class Core implements IdentityListener, UpdateListener {
                                        coreListenerManager.fireRescuingSone(sone);
                                        lockSone(sone);
                                        long edition = sone.getLatestEdition();
-                                       while (!stopped && (edition >= 0) && isSoneRescueMode()) {
+                                       while (!stopped && (edition >= 0) && preferences.isSoneRescueMode()) {
                                                logger.log(Level.FINE, "Downloading edition " + edition + "…");
                                                soneDownloader.fetchSone(sone, sone.getRequestUri().setKeyType("SSK").setDocName("Sone-" + edition));
                                                --edition;
@@ -916,7 +926,12 @@ public class Core implements IdentityListener, UpdateListener {
         * @return The created Sone
         */
        public Sone createSone(OwnIdentity ownIdentity) {
-               identityManager.addContext(ownIdentity, "Sone");
+               try {
+                       ownIdentity.addContext("Sone");
+               } catch (WebOfTrustException wote1) {
+                       logger.log(Level.SEVERE, "Could not add “Sone” context to own identity: " + ownIdentity, wote1);
+                       return null;
+               }
                Sone sone = addLocalSone(ownIdentity);
                return sone;
        }
@@ -966,6 +981,97 @@ public class Core implements IdentityListener, UpdateListener {
        }
 
        /**
+        * Retrieves the trust relationship from the origin to the target. If the
+        * trust relationship can not be retrieved, {@code null} is returned.
+        *
+        * @see Identity#getTrust(OwnIdentity)
+        * @param origin
+        *            The origin of the trust tree
+        * @param target
+        *            The target of the trust
+        * @return The trust relationship
+        */
+       public Trust getTrust(Sone origin, Sone target) {
+               if (!isLocalSone(origin)) {
+                       logger.log(Level.WARNING, "Tried to get trust from remote Sone: %s", origin);
+                       return null;
+               }
+               return target.getIdentity().getTrust((OwnIdentity) origin.getIdentity());
+       }
+
+       /**
+        * Sets the trust value of the given origin Sone for the target Sone.
+        *
+        * @param origin
+        *            The origin Sone
+        * @param target
+        *            The target Sone
+        * @param trustValue
+        *            The trust value (from {@code -100} to {@code 100})
+        */
+       public void setTrust(Sone origin, Sone target, int trustValue) {
+               Validation.begin().isNotNull("Trust Origin", origin).check().isInstanceOf("Trust Origin", origin.getIdentity(), OwnIdentity.class).isNotNull("Trust Target", target).isLessOrEqual("Trust Value", trustValue, 100).isGreaterOrEqual("Trust Value", trustValue, -100).check();
+               try {
+                       ((OwnIdentity) origin.getIdentity()).setTrust(target.getIdentity(), trustValue, preferences.getTrustComment());
+               } catch (WebOfTrustException wote1) {
+                       logger.log(Level.WARNING, "Could not set trust for Sone: " + target, wote1);
+               }
+       }
+
+       /**
+        * Removes any trust assignment for the given target Sone.
+        *
+        * @param origin
+        *            The trust origin
+        * @param target
+        *            The trust target
+        */
+       public void removeTrust(Sone origin, Sone target) {
+               Validation.begin().isNotNull("Trust Origin", origin).isNotNull("Trust Target", target).check().isInstanceOf("Trust Origin Identity", origin.getIdentity(), OwnIdentity.class).check();
+               try {
+                       ((OwnIdentity) origin.getIdentity()).removeTrust(target.getIdentity());
+               } catch (WebOfTrustException wote1) {
+                       logger.log(Level.WARNING, "Could not remove trust for Sone: " + target, wote1);
+               }
+       }
+
+       /**
+        * Assigns the configured positive trust value for the given target.
+        *
+        * @param origin
+        *            The trust origin
+        * @param target
+        *            The trust target
+        */
+       public void trustSone(Sone origin, Sone target) {
+               setTrust(origin, target, preferences.getPositiveTrust());
+       }
+
+       /**
+        * Assigns the configured negative trust value for the given target.
+        *
+        * @param origin
+        *            The trust origin
+        * @param target
+        *            The trust target
+        */
+       public void distrustSone(Sone origin, Sone target) {
+               setTrust(origin, target, preferences.getNegativeTrust());
+       }
+
+       /**
+        * Removes the trust assignment for the given target.
+        *
+        * @param origin
+        *            The trust origin
+        * @param target
+        *            The trust target
+        */
+       public void untrustSone(Sone origin, Sone target) {
+               removeTrust(origin, target);
+       }
+
+       /**
         * Updates the stores Sone with the given Sone.
         *
         * @param sone
@@ -973,7 +1079,7 @@ public class Core implements IdentityListener, UpdateListener {
         */
        public void updateSone(Sone sone) {
                if (hasSone(sone.getId())) {
-                       boolean soneRescueMode = isLocalSone(sone) && isSoneRescueMode();
+                       boolean soneRescueMode = isLocalSone(sone) && preferences.isSoneRescueMode();
                        Sone storedSone = getSone(sone.getId());
                        if (!soneRescueMode && !(sone.getTime() > storedSone.getTime())) {
                                logger.log(Level.FINE, "Downloaded Sone %s is not newer than stored Sone %s.", new Object[] { sone, storedSone });
@@ -988,10 +1094,11 @@ public class Core implements IdentityListener, UpdateListener {
                                                }
                                        }
                                }
+                               List<Post> storedPosts = storedSone.getPosts();
                                synchronized (newPosts) {
                                        for (Post post : sone.getPosts()) {
-                                               post.setSone(getSone(post.getSone().getId()));
-                                               if (!storedSone.getPosts().contains(post) && !knownPosts.contains(post.getId())) {
+                                               post.setSone(storedSone);
+                                               if (!storedPosts.contains(post) && !knownPosts.contains(post.getId())) {
                                                        newPosts.add(post.getId());
                                                        coreListenerManager.fireNewPostFound(post);
                                                }
@@ -1008,10 +1115,11 @@ public class Core implements IdentityListener, UpdateListener {
                                                }
                                        }
                                }
+                               Set<Reply> storedReplies = storedSone.getReplies();
                                synchronized (newReplies) {
                                        for (Reply reply : sone.getReplies()) {
-                                               reply.setSone(getSone(reply.getSone().getId()));
-                                               if (!storedSone.getReplies().contains(reply) && !knownReplies.contains(reply.getId())) {
+                                               reply.setSone(storedSone);
+                                               if (!storedReplies.contains(reply) && !knownReplies.contains(reply.getId())) {
                                                        newReplies.add(reply.getId());
                                                        coreListenerManager.fireNewReplyFound(reply);
                                                }
@@ -1070,8 +1178,12 @@ public class Core implements IdentityListener, UpdateListener {
                        localSones.remove(sone.getId());
                        soneInserters.remove(sone).stop();
                }
-               identityManager.removeContext((OwnIdentity) sone.getIdentity(), "Sone");
-               identityManager.removeProperty((OwnIdentity) sone.getIdentity(), "Sone.LatestEdition");
+               try {
+                       ((OwnIdentity) sone.getIdentity()).removeContext("Sone");
+                       ((OwnIdentity) sone.getIdentity()).removeProperty("Sone.LatestEdition");
+               } catch (WebOfTrustException wote1) {
+                       logger.log(Level.WARNING, "Could not remove context and properties from Sone: " + sone, wote1);
+               }
                try {
                        configuration.getLongValue("Sone/" + sone.getId() + "/Time").setValue(null);
                } catch (ConfigurationException ce1) {
@@ -1080,6 +1192,23 @@ public class Core implements IdentityListener, UpdateListener {
        }
 
        /**
+        * Marks the given Sone as known. If the Sone was {@link #isNewPost(String)
+        * new} before, a {@link CoreListener#markSoneKnown(Sone)} event is fired.
+        *
+        * @param sone
+        *            The Sone to mark as known
+        */
+       public void markSoneKnown(Sone sone) {
+               synchronized (newSones) {
+                       if (newSones.remove(sone.getId())) {
+                               knownSones.add(sone.getId());
+                               coreListenerManager.fireMarkSoneKnown(sone);
+                               saveConfiguration();
+                       }
+               }
+       }
+
+       /**
         * Loads and updates the given Sone from the configuration. If any error is
         * encountered, loading is aborted and the given Sone is not changed.
         *
@@ -1110,6 +1239,17 @@ public class Core implements IdentityListener, UpdateListener {
                profile.setBirthMonth(configuration.getIntValue(sonePrefix + "/Profile/BirthMonth").getValue(null));
                profile.setBirthYear(configuration.getIntValue(sonePrefix + "/Profile/BirthYear").getValue(null));
 
+               /* load profile fields. */
+               while (true) {
+                       String fieldPrefix = sonePrefix + "/Profile/Fields/" + profile.getFields().size();
+                       String fieldName = configuration.getStringValue(fieldPrefix + "/Name").getValue(null);
+                       if (fieldName == null) {
+                               break;
+                       }
+                       String fieldValue = configuration.getStringValue(fieldPrefix + "/Value").getValue("");
+                       profile.addField(fieldName).setValue(fieldValue);
+               }
+
                /* load posts. */
                Set<Post> posts = new HashSet<Post>();
                while (true) {
@@ -1226,8 +1366,9 @@ public class Core implements IdentityListener, UpdateListener {
                }
 
                logger.log(Level.INFO, "Saving Sone: %s", sone);
-               identityManager.setProperty((OwnIdentity) sone.getIdentity(), "Sone.LatestEdition", String.valueOf(sone.getLatestEdition()));
                try {
+                       ((OwnIdentity) sone.getIdentity()).setProperty("Sone.LatestEdition", String.valueOf(sone.getLatestEdition()));
+
                        /* save Sone into configuration. */
                        String sonePrefix = "Sone/" + sone.getId();
                        configuration.getLongValue(sonePrefix + "/Time").setValue(sone.getTime());
@@ -1242,6 +1383,15 @@ public class Core implements IdentityListener, UpdateListener {
                        configuration.getIntValue(sonePrefix + "/Profile/BirthMonth").setValue(profile.getBirthMonth());
                        configuration.getIntValue(sonePrefix + "/Profile/BirthYear").setValue(profile.getBirthYear());
 
+                       /* save profile fields. */
+                       int fieldCounter = 0;
+                       for (Field profileField : profile.getFields()) {
+                               String fieldPrefix = sonePrefix + "/Profile/Fields/" + fieldCounter++;
+                               configuration.getStringValue(fieldPrefix + "/Name").setValue(profileField.getName());
+                               configuration.getStringValue(fieldPrefix + "/Value").setValue(profileField.getValue());
+                       }
+                       configuration.getStringValue(sonePrefix + "/Profile/Fields/" + fieldCounter + "/Name").setValue(null);
+
                        /* save posts. */
                        int postCounter = 0;
                        for (Post post : sone.getPosts()) {
@@ -1289,6 +1439,8 @@ public class Core implements IdentityListener, UpdateListener {
                        logger.log(Level.INFO, "Sone %s saved.", sone);
                } catch (ConfigurationException ce1) {
                        logger.log(Level.WARNING, "Could not save Sone: " + sone, ce1);
+               } catch (WebOfTrustException wote1) {
+                       logger.log(Level.WARNING, "Could not set WoT property for Sone: " + sone, wote1);
                }
        }
 
@@ -1406,6 +1558,50 @@ public class Core implements IdentityListener, UpdateListener {
        }
 
        /**
+        * Bookmarks the given post.
+        *
+        * @param post
+        *            The post to bookmark
+        */
+       public void bookmark(Post post) {
+               bookmarkPost(post.getId());
+       }
+
+       /**
+        * Bookmarks the post with the given ID.
+        *
+        * @param id
+        *            The ID of the post to bookmark
+        */
+       public void bookmarkPost(String id) {
+               synchronized (bookmarkedPosts) {
+                       bookmarkedPosts.add(id);
+               }
+       }
+
+       /**
+        * Removes the given post from the bookmarks.
+        *
+        * @param post
+        *            The post to unbookmark
+        */
+       public void unbookmark(Post post) {
+               unbookmarkPost(post.getId());
+       }
+
+       /**
+        * Removes the post with the given ID from the bookmarks.
+        *
+        * @param id
+        *            The ID of the post to unbookmark
+        */
+       public void unbookmarkPost(String id) {
+               synchronized (bookmarkedPosts) {
+                       bookmarkedPosts.remove(id);
+               }
+       }
+
+       /**
         * Creates a new reply.
         *
         * @param sone
@@ -1561,6 +1757,9 @@ public class Core implements IdentityListener, UpdateListener {
                try {
                        configuration.getIntValue("Option/ConfigurationVersion").setValue(0);
                        configuration.getIntValue("Option/InsertionDelay").setValue(options.getIntegerOption("InsertionDelay").getReal());
+                       configuration.getIntValue("Option/PositiveTrust").setValue(options.getIntegerOption("PositiveTrust").getReal());
+                       configuration.getIntValue("Option/NegativeTrust").setValue(options.getIntegerOption("NegativeTrust").getReal());
+                       configuration.getStringValue("Option/TrustComment").setValue(options.getStringOption("TrustComment").getReal());
                        configuration.getBooleanValue("Option/SoneRescueMode").setValue(options.getBooleanOption("SoneRescueMode").getReal());
                        configuration.getBooleanValue("Option/ClearOnNextRestart").setValue(options.getBooleanOption("ClearOnNextRestart").getReal());
                        configuration.getBooleanValue("Option/ReallyClearOnNextRestart").setValue(options.getBooleanOption("ReallyClearOnNextRestart").getReal());
@@ -1592,6 +1791,15 @@ public class Core implements IdentityListener, UpdateListener {
                                configuration.getStringValue("KnownReplies/" + replyCounter + "/ID").setValue(null);
                        }
 
+                       /* save bookmarked posts. */
+                       int bookmarkedPostCounter = 0;
+                       synchronized (bookmarkedPosts) {
+                               for (String bookmarkedPostId : bookmarkedPosts) {
+                                       configuration.getStringValue("Bookmarks/Post/" + bookmarkedPostCounter++ + "/ID").setValue(bookmarkedPostId);
+                               }
+                       }
+                       configuration.getStringValue("Bookmarks/Post/" + bookmarkedPostCounter++ + "/ID").setValue(null);
+
                        /* now save it. */
                        configuration.save();
 
@@ -1622,6 +1830,9 @@ public class Core implements IdentityListener, UpdateListener {
                        }
 
                }));
+               options.addIntegerOption("PositiveTrust", new DefaultOption<Integer>(75));
+               options.addIntegerOption("NegativeTrust", new DefaultOption<Integer>(-100));
+               options.addStringOption("TrustComment", new DefaultOption<String>("Set from Sone Web Interface"));
                options.addBooleanOption("SoneRescueMode", new DefaultOption<Boolean>(false));
                options.addBooleanOption("ClearOnNextRestart", new DefaultOption<Boolean>(false));
                options.addBooleanOption("ReallyClearOnNextRestart", new DefaultOption<Boolean>(false));
@@ -1638,6 +1849,9 @@ public class Core implements IdentityListener, UpdateListener {
                }
 
                options.getIntegerOption("InsertionDelay").set(configuration.getIntValue("Option/InsertionDelay").getValue(null));
+               options.getIntegerOption("PositiveTrust").set(configuration.getIntValue("Option/PositiveTrust").getValue(null));
+               options.getIntegerOption("NegativeTrust").set(configuration.getIntValue("Option/NegativeTrust").getValue(null));
+               options.getStringOption("TrustComment").set(configuration.getStringValue("Option/TrustComment").getValue(null));
                options.getBooleanOption("SoneRescueMode").set(configuration.getBooleanValue("Option/SoneRescueMode").getValue(null));
 
                /* load known Sones. */
@@ -1676,6 +1890,18 @@ public class Core implements IdentityListener, UpdateListener {
                        }
                }
 
+               /* load bookmarked posts. */
+               int bookmarkedPostCounter = 0;
+               while (true) {
+                       String bookmarkedPostId = configuration.getStringValue("Bookmarks/Post/" + bookmarkedPostCounter++ + "/ID").getValue(null);
+                       if (bookmarkedPostId == null) {
+                               break;
+                       }
+                       synchronized (bookmarkedPosts) {
+                               bookmarkedPosts.add(bookmarkedPostId);
+                       }
+               }
+
        }
 
        /**
@@ -1706,6 +1932,7 @@ public class Core implements IdentityListener, UpdateListener {
        public void ownIdentityAdded(OwnIdentity ownIdentity) {
                logger.log(Level.FINEST, "Adding OwnIdentity: " + ownIdentity);
                if (ownIdentity.hasContext("Sone")) {
+                       trustedIdentities.put(ownIdentity, Collections.synchronizedSet(new HashSet<Identity>()));
                        addLocalSone(ownIdentity);
                }
        }
@@ -1716,14 +1943,16 @@ public class Core implements IdentityListener, UpdateListener {
        @Override
        public void ownIdentityRemoved(OwnIdentity ownIdentity) {
                logger.log(Level.FINEST, "Removing OwnIdentity: " + ownIdentity);
+               trustedIdentities.remove(ownIdentity);
        }
 
        /**
         * {@inheritDoc}
         */
        @Override
-       public void identityAdded(Identity identity) {
+       public void identityAdded(OwnIdentity ownIdentity, Identity identity) {
                logger.log(Level.FINEST, "Adding Identity: " + identity);
+               trustedIdentities.get(ownIdentity).add(identity);
                addRemoteSone(identity);
        }
 
@@ -1731,13 +1960,15 @@ public class Core implements IdentityListener, UpdateListener {
         * {@inheritDoc}
         */
        @Override
-       public void identityUpdated(final Identity identity) {
+       public void identityUpdated(OwnIdentity ownIdentity, final Identity identity) {
                new Thread(new Runnable() {
 
                        @Override
                        @SuppressWarnings("synthetic-access")
                        public void run() {
                                Sone sone = getRemoteSone(identity.getId());
+                               sone.setIdentity(identity);
+                               soneDownloader.addSone(sone);
                                soneDownloader.fetchSone(sone);
                        }
                }).start();
@@ -1747,8 +1978,8 @@ public class Core implements IdentityListener, UpdateListener {
         * {@inheritDoc}
         */
        @Override
-       public void identityRemoved(Identity identity) {
-               /* TODO */
+       public void identityRemoved(OwnIdentity ownIdentity, Identity identity) {
+               trustedIdentities.get(ownIdentity).remove(identity);
        }
 
        //
@@ -1759,8 +1990,195 @@ public class Core implements IdentityListener, UpdateListener {
         * {@inheritDoc}
         */
        @Override
-       public void updateFound(Version version, long releaseTime) {
-               coreListenerManager.fireUpdateFound(version, releaseTime);
+       public void updateFound(Version version, long releaseTime, long latestEdition) {
+               coreListenerManager.fireUpdateFound(version, releaseTime, latestEdition);
+       }
+
+       /**
+        * Convenience interface for external classes that want to access the core’s
+        * configuration.
+        *
+        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+        */
+       public static class Preferences {
+
+               /** The wrapped options. */
+               private final Options options;
+
+               /**
+                * Creates a new preferences object wrapped around the given options.
+                *
+                * @param options
+                *            The options to wrap
+                */
+               public Preferences(Options options) {
+                       this.options = options;
+               }
+
+               /**
+                * Returns the insertion delay.
+                *
+                * @return The insertion delay
+                */
+               public int getInsertionDelay() {
+                       return options.getIntegerOption("InsertionDelay").get();
+               }
+
+               /**
+                * Sets the insertion delay
+                *
+                * @param insertionDelay
+                *            The new insertion delay, or {@code null} to restore it to
+                *            the default value
+                * @return This preferences
+                */
+               public Preferences setInsertionDelay(Integer insertionDelay) {
+                       options.getIntegerOption("InsertionDelay").set(insertionDelay);
+                       return this;
+               }
+
+               /**
+                * Returns the positive trust.
+                *
+                * @return The positive trust
+                */
+               public int getPositiveTrust() {
+                       return options.getIntegerOption("PositiveTrust").get();
+               }
+
+               /**
+                * Sets the positive trust.
+                *
+                * @param positiveTrust
+                *            The new positive trust, or {@code null} to restore it to
+                *            the default vlaue
+                * @return This preferences
+                */
+               public Preferences setPositiveTrust(Integer positiveTrust) {
+                       options.getIntegerOption("PositiveTrust").set(positiveTrust);
+                       return this;
+               }
+
+               /**
+                * Returns the negative trust.
+                *
+                * @return The negative trust
+                */
+               public int getNegativeTrust() {
+                       return options.getIntegerOption("NegativeTrust").get();
+               }
+
+               /**
+                * Sets the negative trust.
+                *
+                * @param negativeTrust
+                *            The negative trust, or {@code null} to restore it to the
+                *            default value
+                * @return The preferences
+                */
+               public Preferences setNegativeTrust(Integer negativeTrust) {
+                       options.getIntegerOption("NegativeTrust").set(negativeTrust);
+                       return this;
+               }
+
+               /**
+                * Returns the trust comment. This is the comment that is set in the web
+                * of trust when a trust value is assigned to an identity.
+                *
+                * @return The trust comment
+                */
+               public String getTrustComment() {
+                       return options.getStringOption("TrustComment").get();
+               }
+
+               /**
+                * Sets the trust comment.
+                *
+                * @param trustComment
+                *            The trust comment, or {@code null} to restore it to the
+                *            default value
+                * @return This preferences
+                */
+               public Preferences setTrustComment(String trustComment) {
+                       options.getStringOption("TrustComment").set(trustComment);
+                       return this;
+               }
+
+               /**
+                * Returns whether the rescue mode is active.
+                *
+                * @return {@code true} if the rescue mode is active, {@code false}
+                *         otherwise
+                */
+               public boolean isSoneRescueMode() {
+                       return options.getBooleanOption("SoneRescueMode").get();
+               }
+
+               /**
+                * Sets whether the rescue mode is active.
+                *
+                * @param soneRescueMode
+                *            {@code true} if the rescue mode is active, {@code false}
+                *            otherwise
+                * @return This preferences
+                */
+               public Preferences setSoneRescueMode(Boolean soneRescueMode) {
+                       options.getBooleanOption("SoneRescueMode").set(soneRescueMode);
+                       return this;
+               }
+
+               /**
+                * Returns whether Sone should clear its settings on the next restart.
+                * In order to be effective, {@link #isReallyClearOnNextRestart()} needs
+                * to return {@code true} as well!
+                *
+                * @return {@code true} if Sone should clear its settings on the next
+                *         restart, {@code false} otherwise
+                */
+               public boolean isClearOnNextRestart() {
+                       return options.getBooleanOption("ClearOnNextRestart").get();
+               }
+
+               /**
+                * Sets whether Sone will clear its settings on the next restart.
+                *
+                * @param clearOnNextRestart
+                *            {@code true} if Sone should clear its settings on the next
+                *            restart, {@code false} otherwise
+                * @return This preferences
+                */
+               public Preferences setClearOnNextRestart(Boolean clearOnNextRestart) {
+                       options.getBooleanOption("ClearOnNextRestart").set(clearOnNextRestart);
+                       return this;
+               }
+
+               /**
+                * Returns whether Sone should really clear its settings on next
+                * restart. This is a confirmation option that needs to be set in
+                * addition to {@link #isClearOnNextRestart()} in order to clear Sone’s
+                * settings on the next restart.
+                *
+                * @return {@code true} if Sone should really clear its settings on the
+                *         next restart, {@code false} otherwise
+                */
+               public boolean isReallyClearOnNextRestart() {
+                       return options.getBooleanOption("ReallyClearOnNextRestart").get();
+               }
+
+               /**
+                * Sets whether Sone should really clear its settings on the next
+                * restart.
+                *
+                * @param reallyClearOnNextRestart
+                *            {@code true} if Sone should really clear its settings on
+                *            the next restart, {@code false} otherwise
+                * @return This preferences
+                */
+               public Preferences setReallyClearOnNextRestart(Boolean reallyClearOnNextRestart) {
+                       options.getBooleanOption("ReallyClearOnNextRestart").set(reallyClearOnNextRestart);
+                       return this;
+               }
+
        }
 
 }
index 5fbb333..fb83c9f 100644 (file)
@@ -135,7 +135,9 @@ public interface CoreListener extends EventListener {
         *            The version that was found
         * @param releaseTime
         *            The release time of the new version
+        * @param latestEdition
+        *            The latest edition of the Sone homepage
         */
-       public void updateFound(Version version, long releaseTime);
+       public void updateFound(Version version, long releaseTime, long latestEdition);
 
 }
index 4fc9532..6dbdc58 100644 (file)
@@ -201,15 +201,17 @@ public class CoreListenerManager extends AbstractListenerManager<Core, CoreListe
        /**
         * Notifies all listeners that a new version was found.
         *
-        * @see CoreListener#updateFound(Version, long)
+        * @see CoreListener#updateFound(Version, long, long)
         * @param version
         *            The new version
         * @param releaseTime
         *            The release time of the new version
+        * @param latestEdition
+        *            The latest edition of the Sone homepage
         */
-       void fireUpdateFound(Version version, long releaseTime) {
+       void fireUpdateFound(Version version, long releaseTime, long latestEdition) {
                for (CoreListener coreListener : getListeners()) {
-                       coreListener.updateFound(version, releaseTime);
+                       coreListener.updateFound(version, releaseTime, latestEdition);
                }
        }
 
index 52c2857..e22f407 100644 (file)
@@ -153,8 +153,8 @@ public class FreenetInterface {
                                @SuppressWarnings("synthetic-access")
                                public void onFoundEdition(long edition, USK key, ObjectContainer objectContainer, ClientContext clientContext, boolean metadata, short codec, byte[] data, boolean newKnownGood, boolean newSlotToo) {
                                        logger.log(Level.FINE, "Found USK update for Sone “%s” at %s, new known good: %s, new slot too: %s.", new Object[] { sone, key, newKnownGood, newSlotToo });
-                                       if (newKnownGood) {
-                                               sone.setLatestEdition(key.suggestedEdition);
+                                       if (edition > sone.getLatestEdition()) {
+                                               sone.setLatestEdition(edition);
                                                new Thread(new Runnable() {
 
                                                        @Override
@@ -176,7 +176,7 @@ public class FreenetInterface {
                                }
                        };
                        soneUskCallbacks.put(sone.getId(), uskCallback);
-                       node.clientCore.uskManager.subscribe(USK.create(sone.getRequestUri()), uskCallback, true, (HighLevelSimpleClientImpl) client);
+                       node.clientCore.uskManager.subscribe(USK.create(sone.getRequestUri()), uskCallback, (System.currentTimeMillis() - sone.getTime()) < 7 * 24 * 60 * 60 * 1000, (HighLevelSimpleClientImpl) client);
                } catch (MalformedURLException mue1) {
                        logger.log(Level.WARNING, "Could not subscribe USK “" + sone.getRequestUri() + "”!", mue1);
                }
index f77c085..b7ece81 100644 (file)
@@ -161,6 +161,9 @@ public class Options {
        /** Holds all {@link Integer} {@link Option}s. */
        private final Map<String, Option<Integer>> integerOptions = Collections.synchronizedMap(new HashMap<String, Option<Integer>>());
 
+       /** Holds all {@link String} {@link Option}s. */
+       private final Map<String, Option<String>> stringOptions = Collections.synchronizedMap(new HashMap<String, Option<String>>());
+
        /**
         * Adds a boolean option.
         *
@@ -213,4 +216,30 @@ public class Options {
                return integerOptions.get(name);
        }
 
+       /**
+        * Adds a {@link String} {@link Option}.
+        *
+        * @param name
+        *            The name of the option
+        * @param stringOption
+        *            The option
+        * @return The given option
+        */
+       public Option<String> addStringOption(String name, Option<String> stringOption) {
+               stringOptions.put(name, stringOption);
+               return stringOption;
+       }
+
+       /**
+        * Returns a {@link String} {@link Option}.
+        *
+        * @param name
+        *            The name of the string option to get
+        * @return The string option, or {@code null} if there is no option with the
+        *         given name
+        */
+       public Option<String> getStringOption(String name) {
+               return stringOptions.get(name);
+       }
+
 }
index 4328f02..7022f5e 100644 (file)
@@ -25,6 +25,7 @@ import java.util.Set;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
+import net.pterodactylus.sone.core.Core.Preferences;
 import net.pterodactylus.sone.core.Core.SoneStatus;
 import net.pterodactylus.sone.data.Client;
 import net.pterodactylus.sone.data.Post;
@@ -92,9 +93,10 @@ public class SoneDownloader extends AbstractService {
         *            The Sone to add
         */
        public void addSone(Sone sone) {
-               if (sones.add(sone)) {
-                       freenetInterface.registerUsk(sone, this);
+               if (!sones.add(sone)) {
+                       freenetInterface.unregisterUsk(sone);
                }
+               freenetInterface.registerUsk(sone, this);
        }
 
        /**
@@ -122,8 +124,8 @@ public class SoneDownloader extends AbstractService {
 
        /**
         * Fetches the updated Sone. This method can be used to fetch a Sone from a
-        * specific URI (which happens when {@link Core#isSoneRescueMode() „Sone
-        * rescue mode“} is active).
+        * specific URI (which happens when {@link Preferences#isSoneRescueMode()
+        * „Sone rescue mode“} is active).
         *
         * @param sone
         *            The Sone to fetch
@@ -131,9 +133,6 @@ public class SoneDownloader extends AbstractService {
         *            The URI to fetch the Sone from
         */
        public void fetchSone(Sone sone, FreenetURI soneUri) {
-               if (core.getSoneStatus(sone) == SoneStatus.downloading) {
-                       return;
-               }
                logger.log(Level.FINE, "Starting fetch for Sone “%s” from %s…", new Object[] { sone, soneUri });
                FreenetURI requestUri = soneUri.setMetaString(new String[] { "sone.xml" });
                core.setSoneStatus(sone, SoneStatus.downloading);
@@ -146,6 +145,7 @@ public class SoneDownloader extends AbstractService {
                        logger.log(Level.FINEST, "Got %d bytes back.", fetchResults.getRight().size());
                        Sone parsedSone = parseSone(sone, fetchResults.getRight(), fetchResults.getLeft());
                        if (parsedSone != null) {
+                               addSone(parsedSone);
                                core.updateSone(parsedSone);
                        }
                } finally {
@@ -285,7 +285,7 @@ public class SoneDownloader extends AbstractService {
                if ((soneInsertUri != null) && (sone.getInsertUri() == null)) {
                        try {
                                sone.setInsertUri(new FreenetURI(soneInsertUri));
-                               sone.setLatestEdition(Math.max(sone.getRequestUri().getSuggestedEdition(), sone.getInsertUri().getSuggestedEdition()));
+                               sone.setLatestEdition(Math.max(sone.getRequestUri().getEdition(), sone.getInsertUri().getEdition()));
                        } catch (MalformedURLException mue1) {
                                /* TODO - mark Sone as bad. */
                                logger.log(Level.WARNING, "Downloaded Sone " + sone + " has invalid insert URI: " + soneInsertUri, mue1);
@@ -310,6 +310,25 @@ public class SoneDownloader extends AbstractService {
                Profile profile = new Profile().setFirstName(profileFirstName).setMiddleName(profileMiddleName).setLastName(profileLastName);
                profile.setBirthDay(profileBirthDay).setBirthMonth(profileBirthMonth).setBirthYear(profileBirthYear);
 
+               /* parse profile fields. */
+               SimpleXML profileFieldsXml = profileXml.getNode("fields");
+               if (profileFieldsXml != null) {
+                       for (SimpleXML fieldXml : profileFieldsXml.getNodes("field")) {
+                               String fieldName = fieldXml.getValue("field-name", null);
+                               String fieldValue = fieldXml.getValue("field-value", null);
+                               if ((fieldName == null) || (fieldValue == null)) {
+                                       logger.log(Level.WARNING, "Downloaded profile field for Sone %s with missing data! Name: %s, Value: %s", new Object[] { sone, fieldName, fieldValue });
+                                       return null;
+                               }
+                               try {
+                                       profile.addField(fieldName).setValue(fieldValue);
+                               } catch (IllegalArgumentException iae1) {
+                                       logger.log(Level.WARNING, "Duplicate field: " + fieldName, iae1);
+                                       return null;
+                               }
+                       }
+               }
+
                /* parse posts. */
                SimpleXML postsXml = soneXml.getNode("posts");
                Set<Post> posts = new HashSet<Post>();
index fec4e8f..b16d374 100644 (file)
@@ -36,10 +36,13 @@ import net.pterodactylus.sone.main.SonePlugin;
 import net.pterodactylus.util.io.Closer;
 import net.pterodactylus.util.logging.Logging;
 import net.pterodactylus.util.service.AbstractService;
-import net.pterodactylus.util.template.DefaultTemplateFactory;
+import net.pterodactylus.util.template.HtmlFilter;
 import net.pterodactylus.util.template.ReflectionAccessor;
 import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.template.TemplateContextFactory;
 import net.pterodactylus.util.template.TemplateException;
+import net.pterodactylus.util.template.TemplateParser;
 import net.pterodactylus.util.template.XmlFilter;
 import freenet.client.async.ManifestElement;
 import freenet.keys.FreenetURI;
@@ -58,11 +61,12 @@ public class SoneInserter extends AbstractService {
        private static volatile int insertionDelay = 60;
 
        /** The template factory used to create the templates. */
-       private static final DefaultTemplateFactory templateFactory = new DefaultTemplateFactory();
+       private static final TemplateContextFactory templateContextFactory = new TemplateContextFactory();
 
        static {
-               templateFactory.addAccessor(Object.class, new ReflectionAccessor());
-               templateFactory.addFilter("xml", new XmlFilter());
+               templateContextFactory.addAccessor(Object.class, new ReflectionAccessor());
+               templateContextFactory.addFilter("xml", new XmlFilter());
+               templateContextFactory.addFilter("html", new HtmlFilter());
        }
 
        /** The UTF-8 charset. */
@@ -182,7 +186,6 @@ public class SoneInserter extends AbstractService {
                                        } else {
                                                lastModificationTime = System.currentTimeMillis();
                                                modified = true;
-                                               sone.setTime(lastModificationTime);
                                                logger.log(Level.FINE, "Sone %s has been modified, waiting %d seconds before inserting.", new Object[] { sone.getName(), insertionDelay });
                                        }
                                        lastFingerprint = fingerprint;
@@ -199,13 +202,17 @@ public class SoneInserter extends AbstractService {
                                boolean success = false;
                                try {
                                        core.setSoneStatus(sone, SoneStatus.inserting);
-                                       FreenetURI finalUri = freenetInterface.insertDirectory(insertInformation.getInsertUri().setKeyType("USK").setSuggestedEdition(0), insertInformation.generateManifestEntries(), "index.html");
+                                       long insertTime = System.currentTimeMillis();
+                                       insertInformation.setTime(insertTime);
+                                       FreenetURI finalUri = freenetInterface.insertDirectory(insertInformation.getInsertUri(), insertInformation.generateManifestEntries(), "index.html");
                                        /* at this point we might already be stopped. */
                                        if (shouldStop()) {
                                                /* if so, bail out, don’t change anything. */
                                                break;
                                        }
+                                       sone.setTime(insertTime);
                                        sone.setLatestEdition(finalUri.getEdition());
+                                       core.saveSone(sone);
                                        success = true;
                                        logger.log(Level.INFO, "Inserted Sone “%s” at %s.", new Object[] { sone.getName(), finalUri });
                                } catch (SoneException se1) {
@@ -276,6 +283,16 @@ public class SoneInserter extends AbstractService {
                        return (FreenetURI) soneProperties.get("insertUri");
                }
 
+               /**
+                * Sets the time of the Sone at the time of the insert.
+                *
+                * @param time
+                *            The time of the Sone
+                */
+               public void setTime(long time) {
+                       soneProperties.put("time", time);
+               }
+
                //
                // ACTIONS
                //
@@ -314,10 +331,11 @@ public class SoneInserter extends AbstractService {
                 */
                @SuppressWarnings("synthetic-access")
                private ManifestElement createManifestElement(String name, String contentType, String templateName) {
-                       InputStreamReader templateInputStreamReader;
-                       Template template = templateFactory.createTemplate(templateInputStreamReader = new InputStreamReader(getClass().getResourceAsStream(templateName), utf8Charset));
+                       InputStreamReader templateInputStreamReader = null;
+                       Template template;
                        try {
-                               template.parse();
+                               templateInputStreamReader = new InputStreamReader(getClass().getResourceAsStream(templateName), utf8Charset);
+                               template = TemplateParser.parse(templateInputStreamReader);
                        } catch (TemplateException te1) {
                                logger.log(Level.SEVERE, "Could not parse template “" + templateName + "”!", te1);
                                return null;
@@ -325,12 +343,13 @@ public class SoneInserter extends AbstractService {
                                Closer.close(templateInputStreamReader);
                        }
 
-                       template.set("currentSone", soneProperties);
-                       template.set("version", SonePlugin.VERSION);
+                       TemplateContext templateContext = templateContextFactory.createTemplateContext();
+                       templateContext.set("currentSone", soneProperties);
+                       templateContext.set("version", SonePlugin.VERSION);
                        StringWriter writer = new StringWriter();
                        StringBucket bucket = null;
                        try {
-                               template.render(writer);
+                               template.render(templateContext, writer);
                                bucket = new StringBucket(writer.toString(), utf8Charset);
                                return new ManifestElement(name, bucket, contentType, bucket.size());
                        } catch (TemplateException te1) {
index 0839cb9..e07e160 100644 (file)
@@ -49,7 +49,7 @@ public class UpdateChecker {
        private static final String SONE_HOMEPAGE = "USK@nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI,DuQSUZiI~agF8c-6tjsFFGuZ8eICrzWCILB60nT8KKo,AQACAAE/sone/";
 
        /** The current latest known edition. */
-       private static final int LATEST_EDITION = 23;
+       private static final int LATEST_EDITION = 25;
 
        /** The Freenet interface. */
        private final FreenetInterface freenetInterface;
@@ -60,6 +60,9 @@ public class UpdateChecker {
        /** The current URI of the homepage. */
        private FreenetURI currentUri;
 
+       /** The latest known edition. */
+       private long latestEdition;
+
        /** The current latest known version. */
        private Version currentLatestVersion = SonePlugin.VERSION;
 
@@ -135,6 +138,15 @@ public class UpdateChecker {
                return latestVersionDate;
        }
 
+       /**
+        * Returns the latest known edition of the Sone homepage.
+        *
+        * @return The latest edition of the Sone homepage
+        */
+       public long getLatestEdition() {
+               return latestEdition;
+       }
+
        //
        // ACTIONS
        //
@@ -163,7 +175,8 @@ public class UpdateChecker {
                                        }
                                        Bucket resultBucket = uriResult.getRight().asBucket();
                                        try {
-                                               parseProperties(resultBucket.getInputStream());
+                                               parseProperties(resultBucket.getInputStream(), edition);
+                                               latestEdition = edition;
                                        } catch (IOException ioe1) {
                                                logger.log(Level.WARNING, "Could not parse sone.properties of " + uri, ioe1);
                                        } finally {
@@ -189,14 +202,16 @@ public class UpdateChecker {
         * Parses the properties of the latest version and fires events, if
         * necessary.
         *
-        * @see UpdateListener#updateFound(Version, long)
-        * @see UpdateListenerManager#fireUpdateFound(Version, long)
+        * @see UpdateListener#updateFound(Version, long, long)
+        * @see UpdateListenerManager#fireUpdateFound(Version, long, long)
         * @param propertiesInputStream
         *            The input stream to parse
+        * @param edition
+        *            The latest edition of the Sone homepage
         * @throws IOException
         *             if an I/O error occured
         */
-       private void parseProperties(InputStream propertiesInputStream) throws IOException {
+       private void parseProperties(InputStream propertiesInputStream, long edition) throws IOException {
                Properties properties = new Properties();
                InputStreamReader inputStreamReader = null;
                try {
@@ -226,7 +241,7 @@ public class UpdateChecker {
                        currentLatestVersion = version;
                        latestVersionDate = releaseTime;
                        logger.log(Level.INFO, "Found new version: %s (%tc)", new Object[] { version, new Date(releaseTime) });
-                       updateListenerManager.fireUpdateFound(version, releaseTime);
+                       updateListenerManager.fireUpdateFound(version, releaseTime, edition);
                }
        }
 
index 1469afa..18f4b88 100644 (file)
@@ -36,7 +36,9 @@ public interface UpdateListener extends EventListener {
         *            The version that was found
         * @param releaseTime
         *            The release time of the version
+        * @param latestEdition
+        *            The latest edition of the Sone homepage
         */
-       public void updateFound(Version version, long releaseTime);
+       public void updateFound(Version version, long releaseTime, long latestEdition);
 
 }
index 3f3b21f..cddf8db 100644 (file)
@@ -45,10 +45,12 @@ public class UpdateListenerManager extends AbstractListenerManager<Void, UpdateL
         *            The new version
         * @param releaseTime
         *            The release time of the new version
+        * @param latestEdition
+        *            The latest edition of the Sone homepage
         */
-       void fireUpdateFound(Version version, long releaseTime) {
+       void fireUpdateFound(Version version, long releaseTime, long latestEdition) {
                for (UpdateListener updateListener : getListeners()) {
-                       updateListener.updateFound(version, releaseTime);
+                       updateListener.updateFound(version, releaseTime, latestEdition);
                }
        }
 
index 7c29430..8d4306d 100644 (file)
 
 package net.pterodactylus.sone.data;
 
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.UUID;
+
+import net.pterodactylus.util.validation.Validation;
+
 /**
  * A profile stores personal information about a {@link Sone}. All information
  * is optional and can be {@code null}.
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
-public class Profile {
-
-       /** Whether the profile was modified. */
-       private volatile boolean modified;
+public class Profile implements Fingerprintable {
 
        /** The first name. */
        private volatile String firstName;
@@ -46,6 +50,9 @@ public class Profile {
        /** The year of the birth date. */
        private volatile Integer birthYear;
 
+       /** Additional fields in the profile. */
+       private final List<Field> fields = Collections.synchronizedList(new ArrayList<Field>());
+
        /**
         * Creates a new empty profile.
         */
@@ -69,6 +76,7 @@ public class Profile {
                this.birthDay = profile.birthDay;
                this.birthMonth = profile.birthMonth;
                this.birthYear = profile.birthYear;
+               this.fields.addAll(profile.fields);
        }
 
        //
@@ -76,18 +84,6 @@ public class Profile {
        //
 
        /**
-        * Returns whether this profile was modified after creation. To clear the
-        * “is modified” flag you need to create a new profile from this one using
-        * the {@link #Profile(Profile)} constructor.
-        *
-        * @return {@code true} if this profile was modified after creation,
-        *         {@code false} otherwise
-        */
-       public boolean isModified() {
-               return modified;
-       }
-
-       /**
         * Returns the first name.
         *
         * @return The first name
@@ -104,7 +100,6 @@ public class Profile {
         * @return This profile (for method chaining)
         */
        public Profile setFirstName(String firstName) {
-               modified |= ((firstName != null) && (!firstName.equals(this.firstName))) || (this.firstName != null);
                this.firstName = firstName;
                return this;
        }
@@ -126,7 +121,6 @@ public class Profile {
         * @return This profile (for method chaining)
         */
        public Profile setMiddleName(String middleName) {
-               modified |= ((middleName != null) && (!middleName.equals(this.middleName))) || (this.middleName != null);
                this.middleName = middleName;
                return this;
        }
@@ -148,7 +142,6 @@ public class Profile {
         * @return This profile (for method chaining)
         */
        public Profile setLastName(String lastName) {
-               modified |= ((lastName != null) && (!lastName.equals(this.lastName))) || (this.lastName != null);
                this.lastName = lastName;
                return this;
        }
@@ -170,7 +163,6 @@ public class Profile {
         * @return This profile (for method chaining)
         */
        public Profile setBirthDay(Integer birthDay) {
-               modified |= ((birthDay != null) && (!birthDay.equals(this.birthDay))) || (this.birthDay != null);
                this.birthDay = birthDay;
                return this;
        }
@@ -192,7 +184,6 @@ public class Profile {
         * @return This profile (for method chaining)
         */
        public Profile setBirthMonth(Integer birthMonth) {
-               modified |= ((birthMonth != null) && (!birthMonth.equals(this.birthMonth))) || (this.birthMonth != null);
                this.birthMonth = birthMonth;
                return this;
        }
@@ -214,9 +205,292 @@ public class Profile {
         * @return This profile (for method chaining)
         */
        public Profile setBirthYear(Integer birthYear) {
-               modified |= ((birthYear != null) && (!birthYear.equals(this.birthYear))) || (this.birthYear != null);
                this.birthYear = birthYear;
                return this;
        }
 
+       /**
+        * Returns the fields of this profile.
+        *
+        * @return The fields of this profile
+        */
+       public List<Field> getFields() {
+               return new ArrayList<Field>(fields);
+       }
+
+       /**
+        * Returns whether this profile contains the given field.
+        *
+        * @param field
+        *            The field to check for
+        * @return {@code true} if this profile contains the field, false otherwise
+        */
+       public boolean hasField(Field field) {
+               return fields.contains(field);
+       }
+
+       /**
+        * Returns the field with the given ID.
+        *
+        * @param fieldId
+        *            The ID of the field to get
+        * @return The field, or {@code null} if this profile does not contain a
+        *         field with the given ID
+        */
+       public Field getFieldById(String fieldId) {
+               Validation.begin().isNotNull("Field ID", fieldId).check();
+               for (Field field : fields) {
+                       if (field.getId().equals(fieldId)) {
+                               return field;
+                       }
+               }
+               return null;
+       }
+
+       /**
+        * Returns the field with the given name.
+        *
+        * @param fieldName
+        *            The name of the field to get
+        * @return The field, or {@code null} if this profile does not contain a
+        *         field with the given name
+        */
+       public Field getFieldByName(String fieldName) {
+               for (Field field : fields) {
+                       if (field.getName().equals(fieldName)) {
+                               return field;
+                       }
+               }
+               return null;
+       }
+
+       /**
+        * Appends a new field to the list of fields.
+        *
+        * @param fieldName
+        *            The name of the new field
+        * @return The new field
+        * @throws IllegalArgumentException
+        *             if the name is not valid
+        */
+       public Field addField(String fieldName) throws IllegalArgumentException {
+               Validation.begin().isNotNull("Field Name", fieldName).check().isGreater("Field Name Length", fieldName.length(), 0).isNull("Field Name Unique", getFieldByName(fieldName)).check();
+               @SuppressWarnings("synthetic-access")
+               Field field = new Field().setName(fieldName);
+               fields.add(field);
+               return field;
+       }
+
+       /**
+        * Moves the given field up one position in the field list. The index of the
+        * field to move must be greater than {@code 0} (because you obviously can
+        * not move the first field further up).
+        *
+        * @param field
+        *            The field to move up
+        */
+       public void moveFieldUp(Field field) {
+               Validation.begin().isNotNull("Field", field).check().is("Field Existing", hasField(field)).isGreater("Field Index", getFieldIndex(field), 0).check();
+               int fieldIndex = getFieldIndex(field);
+               fields.remove(field);
+               fields.add(fieldIndex - 1, field);
+       }
+
+       /**
+        * Moves the given field down one position in the field list. The index of
+        * the field to move must be less than the index of the last field (because
+        * you obviously can not move the last field further down).
+        *
+        * @param field
+        *            The field to move down
+        */
+       public void moveFieldDown(Field field) {
+               Validation.begin().isNotNull("Field", field).check().is("Field Existing", hasField(field)).isLess("Field Index", getFieldIndex(field), fields.size() - 1).check();
+               int fieldIndex = getFieldIndex(field);
+               fields.remove(field);
+               fields.add(fieldIndex + 1, field);
+       }
+
+       /**
+        * Removes the given field.
+        *
+        * @param field
+        *            The field to remove
+        */
+       public void removeField(Field field) {
+               Validation.begin().isNotNull("Field", field).check().is("Field Existing", hasField(field)).check();
+               fields.remove(field);
+       }
+
+       //
+       // PRIVATE METHODS
+       //
+
+       /**
+        * Returns the index of the field with the given name.
+        *
+        * @param field
+        *            The name of the field
+        * @return The index of the field, or {@code -1} if there is no field with
+        *         the given name
+        */
+       private int getFieldIndex(Field field) {
+               return fields.indexOf(field);
+       }
+
+       //
+       // INTERFACE Fingerprintable
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public String getFingerprint() {
+               StringBuilder fingerprint = new StringBuilder();
+               fingerprint.append("Profile(");
+               if (firstName != null) {
+                       fingerprint.append("FirstName(").append(firstName).append(')');
+               }
+               if (middleName != null) {
+                       fingerprint.append("MiddleName(").append(middleName).append(')');
+               }
+               if (lastName != null) {
+                       fingerprint.append("LastName(").append(lastName).append(')');
+               }
+               if (birthDay != null) {
+                       fingerprint.append("BirthDay(").append(birthDay).append(')');
+               }
+               if (birthMonth != null) {
+                       fingerprint.append("BirthMonth(").append(birthMonth).append(')');
+               }
+               if (birthYear != null) {
+                       fingerprint.append("BirthYear(").append(birthYear).append(')');
+               }
+               fingerprint.append("ContactInformation(");
+               for (Field field : fields) {
+                       fingerprint.append(field.getName()).append('(').append(field.getValue()).append(')');
+               }
+               fingerprint.append(")");
+               fingerprint.append(")");
+
+               return fingerprint.toString();
+       }
+
+       /**
+        * Container for a profile field.
+        *
+        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+        */
+       public class Field {
+
+               /** The ID of the field. */
+               private final String id;
+
+               /** The name of the field. */
+               private String name;
+
+               /** The value of the field. */
+               private String value;
+
+               /**
+                * Creates a new field with a random ID.
+                */
+               private Field() {
+                       this(UUID.randomUUID().toString());
+               }
+
+               /**
+                * Creates a new field with the given ID.
+                *
+                * @param id
+                *            The ID of the field
+                */
+               private Field(String id) {
+                       Validation.begin().isNotNull("Field ID", id).check();
+                       this.id = id;
+               }
+
+               /**
+                * Returns the ID of this field.
+                *
+                * @return The ID of this field
+                */
+               public String getId() {
+                       return id;
+               }
+
+               /**
+                * Returns the name of this field.
+                *
+                * @return The name of this field
+                */
+               public String getName() {
+                       return name;
+               }
+
+               /**
+                * Sets the name of this field. The name must not be {@code null} and
+                * must not match any other fields in this profile but my match the name
+                * of this field.
+                *
+                * @param name
+                *            The new name of this field
+                * @return This field
+                */
+               public Field setName(String name) {
+                       Validation.begin().isNotNull("Field Name", name).check().is("Field Unique", (getFieldByName(name) == null) || equals(getFieldByName(name))).check();
+                       this.name = name;
+                       return this;
+               }
+
+               /**
+                * Returns the value of this field.
+                *
+                * @return The value of this field
+                */
+               public String getValue() {
+                       return value;
+               }
+
+               /**
+                * Sets the value of this field. While {@code null} is allowed, no
+                * guarantees are made that {@code null} values are correctly persisted
+                * across restarts of the plugin!
+                *
+                * @param value
+                *            The new value of this field
+                * @return This field
+                */
+               public Field setValue(String value) {
+                       this.value = value;
+                       return this;
+               }
+
+               //
+               // OBJECT METHODS
+               //
+
+               /**
+                * {@inheritDoc}
+                */
+               @Override
+               public boolean equals(Object object) {
+                       if (!(object instanceof Field)) {
+                               return false;
+                       }
+                       Field field = (Field) object;
+                       return id.equals(field.id);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               @Override
+               public int hashCode() {
+                       return id.hashCode();
+               }
+
+       }
+
 }
index 91b1c10..23bbb3e 100644 (file)
@@ -41,7 +41,7 @@ import freenet.keys.FreenetURI;
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
-public class Sone implements Fingerprintable {
+public class Sone implements Fingerprintable, Comparable<Sone> {
 
        /** comparator that sorts Sones by their nice name. */
        public static final Comparator<Sone> NICE_NAME_COMPARATOR = new Comparator<Sone>() {
@@ -180,7 +180,7 @@ public class Sone implements Fingerprintable {
         */
        public Sone setRequestUri(FreenetURI requestUri) {
                if (this.requestUri == null) {
-                       this.requestUri = requestUri.setDocName("Sone").setMetaString(new String[0]);
+                       this.requestUri = requestUri.setKeyType("USK").setDocName("Sone").setMetaString(new String[0]);
                        return this;
                }
                if (!this.requestUri.equalsKeypair(requestUri)) {
@@ -208,7 +208,7 @@ public class Sone implements Fingerprintable {
         */
        public Sone setInsertUri(FreenetURI insertUri) {
                if (this.insertUri == null) {
-                       this.insertUri = insertUri.setDocName("Sone").setMetaString(new String[0]);
+                       this.insertUri = insertUri.setKeyType("USK").setDocName("Sone").setMetaString(new String[0]);
                        return this;
                }
                if (!this.insertUri.equalsKeypair(insertUri)) {
@@ -625,26 +625,7 @@ public class Sone implements Fingerprintable {
        @Override
        public synchronized String getFingerprint() {
                StringBuilder fingerprint = new StringBuilder();
-               fingerprint.append("Profile(");
-               if (profile.getFirstName() != null) {
-                       fingerprint.append("FirstName(").append(profile.getFirstName()).append(')');
-               }
-               if (profile.getMiddleName() != null) {
-                       fingerprint.append("MiddleName(").append(profile.getMiddleName()).append(')');
-               }
-               if (profile.getLastName() != null) {
-                       fingerprint.append("LastName(").append(profile.getLastName()).append(')');
-               }
-               if (profile.getBirthDay() != null) {
-                       fingerprint.append("BirthDay(").append(profile.getBirthDay()).append(')');
-               }
-               if (profile.getBirthMonth() != null) {
-                       fingerprint.append("BirthMonth(").append(profile.getBirthMonth()).append(')');
-               }
-               if (profile.getBirthYear() != null) {
-                       fingerprint.append("BirthYear(").append(profile.getBirthYear()).append(')');
-               }
-               fingerprint.append(")");
+               fingerprint.append(profile.getFingerprint());
 
                fingerprint.append("Posts(");
                for (Post post : getPosts()) {
@@ -686,6 +667,18 @@ public class Sone implements Fingerprintable {
        }
 
        //
+       // INTERFACE Comparable<Sone>
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public int compareTo(Sone sone) {
+               return NICE_NAME_COMPARATOR.compare(this, sone);
+       }
+
+       //
        // OBJECT METHODS
        //
 
index 4d6a744..ef1a183 100644 (file)
@@ -19,8 +19,8 @@ package net.pterodactylus.sone.freenet;
 
 import java.util.Map;
 
-import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.Filter;
+import net.pterodactylus.util.template.TemplateContext;
 import freenet.l10n.BaseL10n;
 
 /**
@@ -48,7 +48,7 @@ public class L10nFilter implements Filter {
         * {@inheritDoc}
         */
        @Override
-       public String format(DataProvider dataProvider, Object data, Map<String, String> parameters) {
+       public String format(TemplateContext templateContext, Object data, Map<String, String> parameters) {
                return l10n.getString(String.valueOf(data));
        }
 
diff --git a/src/main/java/net/pterodactylus/sone/freenet/plugin/ConnectorListener.java b/src/main/java/net/pterodactylus/sone/freenet/plugin/ConnectorListener.java
new file mode 100644 (file)
index 0000000..71710e4
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+ * Sone - ConnectorListener.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.freenet.plugin;
+
+import java.util.EventListener;
+
+
+import freenet.support.SimpleFieldSet;
+import freenet.support.api.Bucket;
+
+/**
+ * Interface for objects that want to be notified if a {@link PluginConnector}
+ * receives a reply from a plugin. As a connection listener is always
+ * {@link PluginConnector#addConnectorListener(String, String, ConnectorListener)
+ * added} for a specific plugin, it will always be notified for replies from the
+ * correct plugin (unless you register the same listener for multiple
+ * plugins—which you subsequently should not do).
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface ConnectorListener extends EventListener {
+
+       /**
+        * A reply was received from the plugin this connection listener was added
+        * for.
+        *
+        * @param pluginConnector
+        *            The plugin connector that received the reply
+        * @param fields
+        *            The fields of the reply
+        * @param data
+        *            The data of the reply (may be null)
+        */
+       public void receivedReply(PluginConnector pluginConnector, SimpleFieldSet fields, Bucket data);
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/freenet/plugin/ConnectorListenerManager.java b/src/main/java/net/pterodactylus/sone/freenet/plugin/ConnectorListenerManager.java
new file mode 100644 (file)
index 0000000..7021332
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+ * Sone - ConnectorListenerManager.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.freenet.plugin;
+
+import net.pterodactylus.util.event.AbstractListenerManager;
+import freenet.support.SimpleFieldSet;
+import freenet.support.api.Bucket;
+
+/**
+ * Manages {@link ConnectorListener}s and fire events.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class ConnectorListenerManager extends AbstractListenerManager<PluginConnector, ConnectorListener> {
+
+       /**
+        * Creates a new manager for {@link ConnectorListener}s.
+        *
+        * @param pluginConnector
+        *            The plugin connector that is the source for all events
+        */
+       public ConnectorListenerManager(PluginConnector pluginConnector) {
+               super(pluginConnector);
+       }
+
+       //
+       // ACTIONS
+       //
+
+       /**
+        * Notifies all registered listeners that a reply from the plugin was
+        * received.
+        *
+        * @param fields
+        *            The fields of the reply
+        * @param data
+        *            The data of the reply (may be null)
+        */
+       public void fireReceivedReply(SimpleFieldSet fields, Bucket data) {
+               for (ConnectorListener connectorListener : getListeners()) {
+                       connectorListener.receivedReply(getSource(), fields, data);
+               }
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/freenet/plugin/PluginConnector.java b/src/main/java/net/pterodactylus/sone/freenet/plugin/PluginConnector.java
new file mode 100644 (file)
index 0000000..e7bf828
--- /dev/null
@@ -0,0 +1,203 @@
+/*
+ * Sone - PluginConnector.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.freenet.plugin;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import net.pterodactylus.util.collection.Pair;
+import freenet.pluginmanager.FredPluginTalker;
+import freenet.pluginmanager.PluginNotFoundException;
+import freenet.pluginmanager.PluginRespirator;
+import freenet.pluginmanager.PluginTalker;
+import freenet.support.SimpleFieldSet;
+import freenet.support.api.Bucket;
+
+/**
+ * Interface for talking to other plugins. Other plugins are identified by their
+ * name and a unique connection identifier.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class PluginConnector implements FredPluginTalker {
+
+       /** The plugin respirator. */
+       private final PluginRespirator pluginRespirator;
+
+       /** Connector listener managers for all plugin connections. */
+       private final Map<Pair<String, String>, ConnectorListenerManager> connectorListenerManagers = Collections.synchronizedMap(new HashMap<Pair<String, String>, ConnectorListenerManager>());
+
+       /**
+        * Creates a new plugin connector.
+        *
+        * @param pluginRespirator
+        *            The plugin respirator
+        */
+       public PluginConnector(PluginRespirator pluginRespirator) {
+               this.pluginRespirator = pluginRespirator;
+       }
+
+       //
+       // LISTENER MANAGEMENT
+       //
+
+       /**
+        * Adds a connection listener for the given plugin connection.
+        *
+        * @param pluginName
+        *            The name of the plugin
+        * @param identifier
+        *            The identifier of the connection
+        * @param connectorListener
+        *            The listener to add
+        */
+       public void addConnectorListener(String pluginName, String identifier, ConnectorListener connectorListener) {
+               getConnectorListenerManager(pluginName, identifier).addListener(connectorListener);
+       }
+
+       /**
+        * Removes a connection listener for the given plugin connection.
+        *
+        * @param pluginName
+        *            The name of the plugin
+        * @param identifier
+        *            The identifier of the connection
+        * @param connectorListener
+        *            The listener to remove
+        */
+       public void removeConnectorListener(String pluginName, String identifier, ConnectorListener connectorListener) {
+               getConnectorListenerManager(pluginName, identifier).removeListener(connectorListener);
+       }
+
+       //
+       // ACTIONS
+       //
+
+       /**
+        * Sends a request to the given plugin.
+        *
+        * @param pluginName
+        *            The name of the plugin
+        * @param identifier
+        *            The identifier of the connection
+        * @param fields
+        *            The fields of the message
+        * @throws PluginException
+        *             if the plugin can not be found
+        */
+       public void sendRequest(String pluginName, String identifier, SimpleFieldSet fields) throws PluginException {
+               sendRequest(pluginName, identifier, fields, null);
+       }
+
+       /**
+        * Sends a request to the given plugin.
+        *
+        * @param pluginName
+        *            The name of the plugin
+        * @param identifier
+        *            The identifier of the connection
+        * @param fields
+        *            The fields of the message
+        * @param data
+        *            The payload of the message (may be null)
+        * @throws PluginException
+        *             if the plugin can not be found
+        */
+       public void sendRequest(String pluginName, String identifier, SimpleFieldSet fields, Bucket data) throws PluginException {
+               getPluginTalker(pluginName, identifier).send(fields, data);
+       }
+
+       //
+       // PRIVATE METHODS
+       //
+
+       /**
+        * Returns the connection listener manager for the given plugin connection,
+        * creating a new one if none does exist yet.
+        *
+        * @param pluginName
+        *            The name of the plugin
+        * @param identifier
+        *            The identifier of the connection
+        * @return The connection listener manager
+        */
+       private ConnectorListenerManager getConnectorListenerManager(String pluginName, String identifier) {
+               return getConnectorListenerManager(pluginName, identifier, true);
+       }
+
+       /**
+        * Returns the connection listener manager for the given plugin connection,
+        * optionally creating a new one if none does exist yet.
+        *
+        * @param pluginName
+        *            The name of the plugin
+        * @param identifier
+        *            The identifier of the connection
+        * @param create
+        *            {@code true} to create a new manager if there is none,
+        *            {@code false} to return {@code null} in that case
+        * @return The connection listener manager, or {@code null} if none existed
+        *         and {@code create} is {@code false}
+        */
+       private ConnectorListenerManager getConnectorListenerManager(String pluginName, String identifier, boolean create) {
+               ConnectorListenerManager connectorListenerManager = connectorListenerManagers.get(new Pair<String, String>(pluginName, identifier));
+               if (create && (connectorListenerManager == null)) {
+                       connectorListenerManager = new ConnectorListenerManager(this);
+                       connectorListenerManagers.put(new Pair<String, String>(pluginName, identifier), connectorListenerManager);
+               }
+               return connectorListenerManager;
+       }
+
+       /**
+        * Returns the plugin talker for the given plugin connection.
+        *
+        * @param pluginName
+        *            The name of the plugin
+        * @param identifier
+        *            The identifier of the connection
+        * @return The plugin talker
+        * @throws PluginException
+        *             if the plugin can not be found
+        */
+       private PluginTalker getPluginTalker(String pluginName, String identifier) throws PluginException {
+               try {
+                       return pluginRespirator.getPluginTalker(this, pluginName, identifier);
+               } catch (PluginNotFoundException pnfe1) {
+                       throw new PluginException(pnfe1);
+               }
+       }
+
+       //
+       // INTERFACE FredPluginTalker
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public void onReply(String pluginName, String identifier, SimpleFieldSet params, Bucket data) {
+               ConnectorListenerManager connectorListenerManager = getConnectorListenerManager(pluginName, identifier, false);
+               if (connectorListenerManager == null) {
+                       /* we don’t care about events for this plugin. */
+                       return;
+               }
+               connectorListenerManager.fireReceivedReply(params, data);
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/freenet/plugin/PluginException.java b/src/main/java/net/pterodactylus/sone/freenet/plugin/PluginException.java
new file mode 100644 (file)
index 0000000..0e1af2b
--- /dev/null
@@ -0,0 +1,68 @@
+/*
+ * Sone - PluginException.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.freenet.plugin;
+
+import net.pterodactylus.sone.freenet.wot.WebOfTrustException;
+
+/**
+ * Exception that signals an error when communicating with a plugin.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class PluginException extends WebOfTrustException {
+
+       /**
+        * Creates a new plugin exception.
+        */
+       public PluginException() {
+               super();
+       }
+
+       /**
+        * Creates a new plugin exception.
+        *
+        * @param message
+        *            The message of the exception
+        */
+       public PluginException(String message) {
+               super(message);
+       }
+
+       /**
+        * Creates a new plugin exception.
+        *
+        * @param cause
+        *            The cause of the exception
+        */
+       public PluginException(Throwable cause) {
+               super(cause);
+       }
+
+       /**
+        * Creates a new plugin exception.
+        *
+        * @param message
+        *            The message of the exception
+        * @param cause
+        *            The cause of the exception
+        */
+       public PluginException(String message, Throwable cause) {
+               super(message, cause);
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/ConnectorListener.java b/src/main/java/net/pterodactylus/sone/freenet/wot/ConnectorListener.java
deleted file mode 100644 (file)
index cf1d1e3..0000000
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Sone - ConnectorListener.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.freenet.wot;
-
-import java.util.EventListener;
-
-import freenet.support.SimpleFieldSet;
-import freenet.support.api.Bucket;
-
-/**
- * Interface for objects that want to be notified if a {@link PluginConnector}
- * receives a reply from a plugin. As a connection listener is always
- * {@link PluginConnector#addConnectorListener(String, String, ConnectorListener)
- * added} for a specific plugin, it will always be notified for replies from the
- * correct plugin (unless you register the same listener for multiple
- * plugins—which you subsequently should not do).
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public interface ConnectorListener extends EventListener {
-
-       /**
-        * A reply was received from the plugin this connection listener was added
-        * for.
-        *
-        * @param pluginConnector
-        *            The plugin connector that received the reply
-        * @param fields
-        *            The fields of the reply
-        * @param data
-        *            The data of the reply (may be null)
-        */
-       public void receivedReply(PluginConnector pluginConnector, SimpleFieldSet fields, Bucket data);
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/ConnectorListenerManager.java b/src/main/java/net/pterodactylus/sone/freenet/wot/ConnectorListenerManager.java
deleted file mode 100644 (file)
index 3962eef..0000000
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Sone - ConnectorListenerManager.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.freenet.wot;
-
-import net.pterodactylus.util.event.AbstractListenerManager;
-import freenet.support.SimpleFieldSet;
-import freenet.support.api.Bucket;
-
-/**
- * Manages {@link ConnectorListener}s and fire events.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class ConnectorListenerManager extends AbstractListenerManager<PluginConnector, ConnectorListener> {
-
-       /**
-        * Creates a new manager for {@link ConnectorListener}s.
-        *
-        * @param pluginConnector
-        *            The plugin connector that is the source for all events
-        */
-       public ConnectorListenerManager(PluginConnector pluginConnector) {
-               super(pluginConnector);
-       }
-
-       //
-       // ACTIONS
-       //
-
-       /**
-        * Notifies all registered listeners that a reply from the plugin was
-        * received.
-        *
-        * @param fields
-        *            The fields of the reply
-        * @param data
-        *            The data of the reply (may be null)
-        */
-       public void fireReceivedReply(SimpleFieldSet fields, Bucket data) {
-               for (ConnectorListener connectorListener : getListeners()) {
-                       connectorListener.receivedReply(getSource(), fields, data);
-               }
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/DefaultIdentity.java b/src/main/java/net/pterodactylus/sone/freenet/wot/DefaultIdentity.java
new file mode 100644 (file)
index 0000000..3c04823
--- /dev/null
@@ -0,0 +1,303 @@
+/*
+ * Sone - DefaultIdentity.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.freenet.wot;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import net.pterodactylus.sone.freenet.plugin.PluginException;
+import net.pterodactylus.util.cache.CacheException;
+import net.pterodactylus.util.cache.CacheItem;
+import net.pterodactylus.util.cache.DefaultCacheItem;
+import net.pterodactylus.util.cache.MemoryCache;
+import net.pterodactylus.util.cache.ValueRetriever;
+import net.pterodactylus.util.cache.WritableCache;
+import net.pterodactylus.util.collection.TimedMap;
+import net.pterodactylus.util.logging.Logging;
+
+/**
+ * A Web of Trust identity.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class DefaultIdentity implements Identity {
+
+       /** The logger. */
+       private static final Logger logger = Logging.getLogger(DefaultIdentity.class);
+
+       /** The web of trust connector. */
+       private final WebOfTrustConnector webOfTrustConnector;
+
+       /** The ID of the identity. */
+       private final String id;
+
+       /** The nickname of the identity. */
+       private final String nickname;
+
+       /** The request URI of the identity. */
+       private final String requestUri;
+
+       /** The contexts of the identity. */
+       private final Set<String> contexts = Collections.synchronizedSet(new HashSet<String>());
+
+       /** The properties of the identity. */
+       private final Map<String, String> properties = Collections.synchronizedMap(new HashMap<String, String>());
+
+       /** Cached trust. */
+       private final WritableCache<OwnIdentity, Trust> trustCache = new MemoryCache<OwnIdentity, Trust>(new ValueRetriever<OwnIdentity, Trust>() {
+
+               @Override
+               @SuppressWarnings("synthetic-access")
+               public CacheItem<Trust> retrieve(OwnIdentity ownIdentity) throws CacheException {
+                       try {
+                               return new DefaultCacheItem<Trust>(webOfTrustConnector.getTrust(ownIdentity, DefaultIdentity.this));
+                       } catch (PluginException pe1) {
+                               throw new CacheException("Could not retrieve trust for OwnIdentity: " + ownIdentity, pe1);
+                       }
+               }
+
+       }, new TimedMap<OwnIdentity, CacheItem<Trust>>(60 * 60 * 1000));
+
+       /**
+        * Creates a new identity.
+        *
+        * @param webOfTrustConnector
+        *            The web of trust connector
+        * @param id
+        *            The ID of the identity
+        * @param nickname
+        *            The nickname of the identity
+        * @param requestUri
+        *            The request URI of the identity
+        */
+       public DefaultIdentity(WebOfTrustConnector webOfTrustConnector, String id, String nickname, String requestUri) {
+               this.webOfTrustConnector = webOfTrustConnector;
+               this.id = id;
+               this.nickname = nickname;
+               this.requestUri = requestUri;
+       }
+
+       //
+       // ACCESSORS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public String getId() {
+               return id;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public String getNickname() {
+               return nickname;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public String getRequestUri() {
+               return requestUri;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public Set<String> getContexts() {
+               return Collections.unmodifiableSet(contexts);
+       }
+
+       /**
+        * Sets the contexts of this identity.
+        * <p>
+        * This method is only called by the {@link IdentityManager}.
+        *
+        * @param contexts
+        *            The contexts to set
+        */
+       void setContextsPrivate(Set<String> contexts) {
+               this.contexts.clear();
+               this.contexts.addAll(contexts);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public boolean hasContext(String context) {
+               return contexts.contains(context);
+       }
+
+       /**
+        * Adds the given context to this identity.
+        * <p>
+        * This method is only called by the {@link IdentityManager}.
+        *
+        * @param context
+        *            The context to add
+        */
+       void addContextPrivate(String context) {
+               contexts.add(context);
+       }
+
+       /**
+        * Removes the given context from this identity.
+        * <p>
+        * This method is only called by the {@link IdentityManager}.
+        *
+        * @param context
+        *            The context to remove
+        */
+       public void removeContextPrivate(String context) {
+               contexts.remove(context);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public Map<String, String> getProperties() {
+               synchronized (properties) {
+                       return Collections.unmodifiableMap(properties);
+               }
+       }
+
+       /**
+        * Sets all properties of this identity.
+        * <p>
+        * This method is only called by the {@link IdentityManager}.
+        *
+        * @param properties
+        *            The new properties of this identity
+        */
+       void setPropertiesPrivate(Map<String, String> properties) {
+               synchronized (this.properties) {
+                       this.properties.clear();
+                       this.properties.putAll(properties);
+               }
+       }
+
+       /**
+        * Sets the property with the given name to the given value.
+        * <p>
+        * This method is only called by the {@link IdentityManager}.
+        *
+        * @param name
+        *            The name of the property
+        * @param value
+        *            The value of the property
+        */
+       void setPropertyPrivate(String name, String value) {
+               synchronized (properties) {
+                       properties.put(name, value);
+               }
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public String getProperty(String name) {
+               synchronized (properties) {
+                       return properties.get(name);
+               }
+       }
+
+       /**
+        * Removes the property with the given name.
+        * <p>
+        * This method is only called by the {@link IdentityManager}.
+        *
+        * @param name
+        *            The name of the property to remove
+        */
+       void removePropertyPrivate(String name) {
+               synchronized (properties) {
+                       properties.remove(name);
+               }
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public Trust getTrust(OwnIdentity ownIdentity) {
+               try {
+                       return trustCache.get(ownIdentity);
+               } catch (CacheException ce1) {
+                       logger.log(Level.WARNING, "Could not get trust for OwnIdentity: " + ownIdentity, ce1);
+                       return null;
+               }
+       }
+
+       /**
+        * Sets the trust received for this identity by the given own identity.
+        *
+        * @param ownIdentity
+        *            The own identity that gives the trust
+        * @param trust
+        *            The trust received for this identity
+        */
+       void setTrustPrivate(OwnIdentity ownIdentity, Trust trust) {
+               trustCache.put(ownIdentity, trust);
+       }
+
+       //
+       // OBJECT METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public int hashCode() {
+               return id.hashCode();
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public boolean equals(Object object) {
+               if (!(object instanceof DefaultIdentity)) {
+                       return false;
+               }
+               DefaultIdentity identity = (DefaultIdentity) object;
+               return identity.id.equals(id);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public String toString() {
+               return getClass().getSimpleName() + "[id=" + id + ",nickname=" + nickname + ",contexts=" + contexts + ",properties=" + properties + "]";
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/DefaultOwnIdentity.java b/src/main/java/net/pterodactylus/sone/freenet/wot/DefaultOwnIdentity.java
new file mode 100644 (file)
index 0000000..ab96756
--- /dev/null
@@ -0,0 +1,170 @@
+/*
+ * Sone - DefaultOwnIdentity.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.freenet.wot;
+
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+import net.pterodactylus.util.validation.Validation;
+
+/**
+ * An own identity is an identity that the owner of the node has full control
+ * over.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class DefaultOwnIdentity extends DefaultIdentity implements OwnIdentity {
+
+       /** The identity manager. */
+       private final WebOfTrustConnector webOfTrustConnector;
+
+       /** The insert URI of the identity. */
+       private final String insertUri;
+
+       /**
+        * Creates a new own identity.
+        *
+        * @param webOfTrustConnector
+        *            The identity manager
+        * @param id
+        *            The ID of the identity
+        * @param nickname
+        *            The nickname of the identity
+        * @param requestUri
+        *            The request URI of the identity
+        * @param insertUri
+        *            The insert URI of the identity
+        */
+       public DefaultOwnIdentity(WebOfTrustConnector webOfTrustConnector, String id, String nickname, String requestUri, String insertUri) {
+               super(webOfTrustConnector, id, nickname, requestUri);
+               this.webOfTrustConnector = webOfTrustConnector;
+               this.insertUri = insertUri;
+       }
+
+       //
+       // ACCESSORS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public String getInsertUri() {
+               return insertUri;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public void addContext(String context) throws WebOfTrustException {
+               webOfTrustConnector.addContext(this, context);
+               addContextPrivate(context);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public void removeContext(String context) throws WebOfTrustException {
+               webOfTrustConnector.removeContext(this, context);
+               removeContextPrivate(context);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public void setContexts(Set<String> contexts) throws WebOfTrustException {
+               for (String context : getContexts()) {
+                       if (!contexts.contains(context)) {
+                               webOfTrustConnector.removeContext(this, context);
+                       }
+               }
+               for (String context : contexts) {
+                       if (!getContexts().contains(context)) {
+                               webOfTrustConnector.addContext(this, context);
+                       }
+               }
+               setContextsPrivate(contexts);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public void setProperty(String name, String value) throws WebOfTrustException {
+               webOfTrustConnector.setProperty(this, name, value);
+               setPropertyPrivate(name, value);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public void removeProperty(String name) throws WebOfTrustException {
+               webOfTrustConnector.removeProperty(this, name);
+               removePropertyPrivate(name);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public void setProperties(Map<String, String> properties) throws WebOfTrustException {
+               for (Entry<String, String> oldProperty : getProperties().entrySet()) {
+                       if (!properties.containsKey(oldProperty.getKey())) {
+                               webOfTrustConnector.removeProperty(this, oldProperty.getKey());
+                       } else {
+                               webOfTrustConnector.setProperty(this, oldProperty.getKey(), properties.get(oldProperty.getKey()));
+                       }
+               }
+               for (Entry<String, String> newProperty : properties.entrySet()) {
+                       if (!getProperties().containsKey(newProperty.getKey())) {
+                               webOfTrustConnector.setProperty(this, newProperty.getKey(), newProperty.getValue());
+                       }
+               }
+               setPropertiesPrivate(properties);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public void setTrust(Identity target, int trustValue, String comment) throws WebOfTrustException {
+               Validation.begin().isNotNull("Trust Target", target).isNotNull("Trust Comment", comment).isLessOrEqual("Trust Value", trustValue, 100).isGreaterOrEqual("Trust Value", trustValue, -100).check();
+               webOfTrustConnector.setTrust(this, target, trustValue, comment);
+               if (target instanceof DefaultIdentity) {
+                       ((DefaultIdentity) target).setTrustPrivate(this, new Trust(trustValue, trustValue, 0));
+               }
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public void removeTrust(Identity target) throws WebOfTrustException {
+               Validation.begin().isNotNull("Trust Target", target).check();
+               webOfTrustConnector.removeTrust(this, target);
+               if (target instanceof DefaultIdentity) {
+                       ((DefaultIdentity) target).setTrustPrivate(this, new Trust(null, null, null));
+               }
+       }
+
+}
index 5816b47..1cf1644 100644 (file)
 
 package net.pterodactylus.sone.freenet.wot;
 
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
 
 /**
- * A Web of Trust identity.
+ * Interface for web of trust identities, defining all functions that can be
+ * performed on an identity. The identity is the main entry point for identity
+ * management.
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
-public class Identity {
-
-       /** The ID of the identity. */
-       private final String id;
-
-       /** The nickname of the identity. */
-       private final String nickname;
-
-       /** The request URI of the identity. */
-       private final String requestUri;
-
-       /** The contexts of the identity. */
-       private final Set<String> contexts = Collections.synchronizedSet(new HashSet<String>());
-
-       /** The properties of the identity. */
-       private final Map<String, String> properties = Collections.synchronizedMap(new HashMap<String, String>());
-
-       /**
-        * Creates a new identity.
-        *
-        * @param id
-        *            The ID of the identity
-        * @param nickname
-        *            The nickname of the identity
-        * @param requestUri
-        *            The request URI of the identity
-        */
-       public Identity(String id, String nickname, String requestUri) {
-               this.id = id;
-               this.nickname = nickname;
-               this.requestUri = requestUri;
-       }
-
-       //
-       // ACCESSORS
-       //
+public interface Identity {
 
        /**
         * Returns the ID of the identity.
         *
         * @return The ID of the identity
         */
-       public String getId() {
-               return id;
-       }
+       public String getId();
 
        /**
         * Returns the nickname of the identity.
         *
         * @return The nickname of the identity
         */
-       public String getNickname() {
-               return nickname;
-       }
+       public String getNickname();
 
        /**
         * Returns the request URI of the identity.
         *
         * @return The request URI of the identity
         */
-       public String getRequestUri() {
-               return requestUri;
-       }
+       public String getRequestUri();
 
        /**
         * Returns all contexts of this identity.
         *
         * @return All contexts of this identity
         */
-       public Set<String> getContexts() {
-               return Collections.unmodifiableSet(contexts);
-       }
-
-       /**
-        * Sets all contexts of this identity.
-        * <p>
-        * This method is only called by the {@link IdentityManager}.
-        *
-        * @param contexts
-        *            All contexts of the identity
-        */
-       void setContexts(Set<String> contexts) {
-               this.contexts.clear();
-               this.contexts.addAll(contexts);
-       }
+       public Set<String> getContexts();
 
        /**
         * Returns whether this identity has the given context.
@@ -122,75 +65,14 @@ public class Identity {
         * @return {@code true} if this identity has the given context,
         *         {@code false} otherwise
         */
-       public boolean hasContext(String context) {
-               return contexts.contains(context);
-       }
-
-       /**
-        * Adds the given context to this identity.
-        * <p>
-        * This method is only called by the {@link IdentityManager}.
-        *
-        * @param context
-        *            The context to add
-        */
-       void addContext(String context) {
-               contexts.add(context);
-       }
-
-       /**
-        * Removes the given context from this identity.
-        * <p>
-        * This method is only called by the {@link IdentityManager}.
-        *
-        * @param context
-        *            The context to remove
-        */
-       void removeContext(String context) {
-               contexts.remove(context);
-       }
+       public boolean hasContext(String context);
 
        /**
         * Returns all properties of this identity.
         *
         * @return All properties of this identity
         */
-       public Map<String, String> getProperties() {
-               synchronized (properties) {
-                       return Collections.unmodifiableMap(properties);
-               }
-       }
-
-       /**
-        * Sets all properties of this identity.
-        * <p>
-        * This method is only called by the {@link IdentityManager}.
-        *
-        * @param properties
-        *            The new properties of this identity
-        */
-       void setProperties(Map<String, String> properties) {
-               synchronized (this.properties) {
-                       this.properties.clear();
-                       this.properties.putAll(properties);
-               }
-       }
-
-       /**
-        * Sets the property with the given name to the given value.
-        * <p>
-        * This method is only called by the {@link IdentityManager}.
-        *
-        * @param name
-        *            The name of the property
-        * @param value
-        *            The value of the property
-        */
-       void setProperty(String name, String value) {
-               synchronized (properties) {
-                       properties.put(name, value);
-               }
-       }
+       public Map<String, String> getProperties();
 
        /**
         * Returns the value of the property with the given name.
@@ -199,56 +81,19 @@ public class Identity {
         *            The name of the property
         * @return The value of the property
         */
-       public String getProperty(String name) {
-               synchronized (properties) {
-                       return properties.get(name);
-               }
-       }
+       public String getProperty(String name);
 
        /**
-        * Removes the property with the given name.
-        * <p>
-        * This method is only called by the {@link IdentityManager}.
+        * Retrieves the trust that this identity receives from the given own
+        * identity. If this identity is not in the own identity’s trust tree, a
+        * {@link Trust} is returned that has all its elements set to {@code null}.
+        * If the trust can not be retrieved, {@code null} is returned.
         *
-        * @param name
-        *            The name of the property to remove
-        */
-       void removeProperty(String name) {
-               synchronized (properties) {
-                       properties.remove(name);
-               }
-       }
-
-       //
-       // OBJECT METHODS
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public int hashCode() {
-               return id.hashCode();
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public boolean equals(Object object) {
-               if (!(object instanceof Identity)) {
-                       return false;
-               }
-               Identity identity = (Identity) object;
-               return identity.id.equals(id);
-       }
-
-       /**
-        * {@inheritDoc}
+        * @param ownIdentity
+        *            The own identity to get the trust for
+        * @return The trust assigned to this identity, or {@code null} if the trust
+        *         could not be retrieved
         */
-       @Override
-       public String toString() {
-               return getClass().getSimpleName() + "[id=" + id + ",nickname=" + nickname + ",contexts=" + contexts + ",properties=" + properties + "]";
-       }
+       public Trust getTrust(OwnIdentity ownIdentity);
 
 }
index 64ea61f..3721f49 100644 (file)
@@ -47,25 +47,31 @@ public interface IdentityListener extends EventListener {
        /**
         * Notifies a listener that a new identity was discovered.
         *
+        * @param ownIdentity
+        *            The own identity at the root of the trust tree
         * @param identity
         *            The new identity
         */
-       public void identityAdded(Identity identity);
+       public void identityAdded(OwnIdentity ownIdentity, Identity identity);
 
        /**
         * Notifies a listener that some properties of the identity have changed.
         *
+        * @param ownIdentity
+        *            The own identity at the root of the trust tree
         * @param identity
         *            The updated identity
         */
-       public void identityUpdated(Identity identity);
+       public void identityUpdated(OwnIdentity ownIdentity, Identity identity);
 
        /**
         * Notifies a listener that an identity has gone away.
         *
+        * @param ownIdentity
+        *            The own identity at the root of the trust tree
         * @param identity
         *            The disappeared identity
         */
-       public void identityRemoved(Identity identity);
+       public void identityRemoved(OwnIdentity ownIdentity, Identity identity);
 
 }
index c08fd16..c6ea783 100644 (file)
@@ -68,39 +68,45 @@ public class IdentityListenerManager extends AbstractListenerManager<IdentityMan
        /**
         * Notifies all listeners that a new identity was discovered.
         *
-        * @see IdentityListener#identityAdded(Identity)
+        * @see IdentityListener#identityAdded(OwnIdentity, Identity)
+        * @param ownIdentity
+        *            The own identity at the root of the trust tree
         * @param identity
         *            The new identity
         */
-       public void fireIdentityAdded(Identity identity) {
+       public void fireIdentityAdded(OwnIdentity ownIdentity, Identity identity) {
                for (IdentityListener identityListener : getListeners()) {
-                       identityListener.identityAdded(identity);
+                       identityListener.identityAdded(ownIdentity, identity);
                }
        }
 
        /**
         * Notifies all listeners that some properties of the identity have changed.
         *
-        * @see IdentityListener#identityUpdated(Identity)
+        * @see IdentityListener#identityUpdated(OwnIdentity, Identity)
+        * @param ownIdentity
+        *            The own identity at the root of the trust tree
         * @param identity
         *            The updated identity
         */
-       public void fireIdentityUpdated(Identity identity) {
+       public void fireIdentityUpdated(OwnIdentity ownIdentity, Identity identity) {
                for (IdentityListener identityListener : getListeners()) {
-                       identityListener.identityUpdated(identity);
+                       identityListener.identityUpdated(ownIdentity, identity);
                }
        }
 
        /**
         * Notifies all listeners that an identity has gone away.
         *
-        * @see IdentityListener#identityRemoved(Identity)
+        * @see IdentityListener#identityRemoved(OwnIdentity, Identity)
+        * @param ownIdentity
+        *            The own identity at the root of the trust tree
         * @param identity
         *            The disappeared identity
         */
-       public void fireIdentityRemoved(Identity identity) {
+       public void fireIdentityRemoved(OwnIdentity ownIdentity, Identity identity) {
                for (IdentityListener identityListener : getListeners()) {
-                       identityListener.identityRemoved(identity);
+                       identityListener.identityRemoved(ownIdentity, identity);
                }
        }
 
index 7796557..98ba2c7 100644 (file)
@@ -25,6 +25,7 @@ import java.util.Set;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
+import net.pterodactylus.sone.freenet.plugin.PluginException;
 import net.pterodactylus.util.logging.Logging;
 import net.pterodactylus.util.service.AbstractService;
 
@@ -159,93 +160,13 @@ public class IdentityManager extends AbstractService {
                        }
                        checkOwnIdentities(newOwnIdentities);
                        return ownIdentities;
-               } catch (PluginException pe1) {
-                       logger.log(Level.WARNING, "Could not load all own identities!", pe1);
+               } catch (WebOfTrustException wote1) {
+                       logger.log(Level.WARNING, "Could not load all own identities!", wote1);
                        return Collections.emptySet();
                }
        }
 
        //
-       // ACTIONS
-       //
-
-       /**
-        * Adds a context to the given own identity.
-        *
-        * @param ownIdentity
-        *            The own identity
-        * @param context
-        *            The context to add
-        */
-       public void addContext(OwnIdentity ownIdentity, String context) {
-               if (ownIdentity.hasContext(context)) {
-                       return;
-               }
-               try {
-                       webOfTrustConnector.addContext(ownIdentity, context);
-                       ownIdentity.addContext(context);
-               } catch (PluginException pe1) {
-                       logger.log(Level.WARNING, "Could not add context " + context + " to OwnIdentity " + ownIdentity + ".", pe1);
-               }
-       }
-
-       /**
-        * Removes a context from the given own identity.
-        *
-        * @param ownIdentity
-        *            The own identity
-        * @param context
-        *            The context to remove
-        */
-       public void removeContext(OwnIdentity ownIdentity, String context) {
-               if (!ownIdentity.hasContext(context)) {
-                       return;
-               }
-               try {
-                       webOfTrustConnector.removeContext(ownIdentity, context);
-                       ownIdentity.removeContext(context);
-               } catch (PluginException pe1) {
-                       logger.log(Level.WARNING, "Could not remove context " + context + " from OwnIdentity " + ownIdentity + ".", pe1);
-               }
-       }
-
-       /**
-        * Sets the property with the given name to the given value.
-        *
-        * @param ownIdentity
-        *            The own identity
-        * @param name
-        *            The name of the property
-        * @param value
-        *            The value of the property
-        */
-       public void setProperty(OwnIdentity ownIdentity, String name, String value) {
-               try {
-                       webOfTrustConnector.setProperty(ownIdentity, name, value);
-                       ownIdentity.setProperty(name, value);
-               } catch (PluginException pe1) {
-                       logger.log(Level.WARNING, "Could not set property “" + name + "” to “" + value + "” for OwnIdentity: " + ownIdentity, pe1);
-               }
-       }
-
-       /**
-        * Removes the property with the given name.
-        *
-        * @param ownIdentity
-        *            The own identity
-        * @param name
-        *            The name of the property to remove
-        */
-       public void removeProperty(OwnIdentity ownIdentity, String name) {
-               try {
-                       webOfTrustConnector.removeProperty(ownIdentity, name);
-                       ownIdentity.removeProperty(name);
-               } catch (PluginException pe1) {
-                       logger.log(Level.WARNING, "Could not remove property “" + name + "” from OwnIdentity: " + ownIdentity, pe1);
-               }
-       }
-
-       //
        // SERVICE METHODS
        //
 
@@ -254,15 +175,14 @@ public class IdentityManager extends AbstractService {
         */
        @Override
        protected void serviceRun() {
-               Map<String, Identity> oldIdentities = Collections.emptyMap();
+               Map<OwnIdentity, Map<String, Identity>> oldIdentities = Collections.emptyMap();
                while (!shouldStop()) {
-                       Map<String, Identity> currentIdentities = new HashMap<String, Identity>();
+                       Map<OwnIdentity, Map<String, Identity>> currentIdentities = new HashMap<OwnIdentity, Map<String, Identity>>();
                        Map<String, OwnIdentity> currentOwnIdentities = new HashMap<String, OwnIdentity>();
 
-                       /* get all identities with the wanted context from WoT. */
-                       Set<OwnIdentity> ownIdentities;
                        try {
-                               ownIdentities = webOfTrustConnector.loadAllOwnIdentities();
+                               /* get all identities with the wanted context from WoT. */
+                               Set<OwnIdentity> ownIdentities = webOfTrustConnector.loadAllOwnIdentities();
 
                                /* check for changes. */
                                for (OwnIdentity ownIdentity : ownIdentities) {
@@ -271,76 +191,80 @@ public class IdentityManager extends AbstractService {
                                checkOwnIdentities(currentOwnIdentities);
 
                                /* now filter for context and get all identities. */
-                               currentOwnIdentities.clear();
                                for (OwnIdentity ownIdentity : ownIdentities) {
                                        if ((context != null) && !ownIdentity.hasContext(context)) {
                                                continue;
                                        }
-                                       currentOwnIdentities.put(ownIdentity.getId(), ownIdentity);
-                                       for (Identity identity : webOfTrustConnector.loadTrustedIdentities(ownIdentity, context)) {
-                                               currentIdentities.put(identity.getId(), identity);
-                                       }
-                               }
 
-                               /* find removed identities. */
-                               for (Identity oldIdentity : oldIdentities.values()) {
-                                       if (!currentIdentities.containsKey(oldIdentity.getId())) {
-                                               identityListenerManager.fireIdentityRemoved(oldIdentity);
+                                       Set<Identity> trustedIdentities = webOfTrustConnector.loadTrustedIdentities(ownIdentity, context);
+                                       Map<String, Identity> identities = new HashMap<String, Identity>();
+                                       currentIdentities.put(ownIdentity, identities);
+                                       for (Identity identity : trustedIdentities) {
+                                               identities.put(identity.getId(), identity);
                                        }
-                               }
 
-                               /* find new identities. */
-                               for (Identity currentIdentity : currentIdentities.values()) {
-                                       if (!oldIdentities.containsKey(currentIdentity.getId())) {
-                                               identityListenerManager.fireIdentityAdded(currentIdentity);
+                                       /* find new identities. */
+                                       for (Identity currentIdentity : currentIdentities.get(ownIdentity).values()) {
+                                               if (!oldIdentities.containsKey(ownIdentity) || !oldIdentities.get(ownIdentity).containsKey(currentIdentity.getId())) {
+                                                       identityListenerManager.fireIdentityAdded(ownIdentity, currentIdentity);
+                                               }
                                        }
-                               }
 
-                               /* check for changes in the contexts. */
-                               for (Identity oldIdentity : oldIdentities.values()) {
-                                       if (!currentIdentities.containsKey(oldIdentity.getId())) {
-                                               continue;
-                                       }
-                                       Identity newIdentity = currentIdentities.get(oldIdentity.getId());
-                                       Set<String> oldContexts = oldIdentity.getContexts();
-                                       Set<String> newContexts = newIdentity.getContexts();
-                                       if (oldContexts.size() != newContexts.size()) {
-                                               identityListenerManager.fireIdentityUpdated(newIdentity);
-                                               continue;
-                                       }
-                                       for (String oldContext : oldContexts) {
-                                               if (!newContexts.contains(oldContext)) {
-                                                       identityListenerManager.fireIdentityUpdated(newIdentity);
-                                                       break;
+                                       /* find removed identities. */
+                                       if (oldIdentities.containsKey(ownIdentity)) {
+                                               for (Identity oldIdentity : oldIdentities.get(ownIdentity).values()) {
+                                                       if (!currentIdentities.get(ownIdentity).containsKey(oldIdentity.getId())) {
+                                                               identityListenerManager.fireIdentityRemoved(ownIdentity, oldIdentity);
+                                                       }
                                                }
-                                       }
-                               }
 
-                               /* check for changes in the properties. */
-                               for (Identity oldIdentity : oldIdentities.values()) {
-                                       if (!currentIdentities.containsKey(oldIdentity.getId())) {
-                                               continue;
-                                       }
-                                       Identity newIdentity = currentIdentities.get(oldIdentity.getId());
-                                       Map<String, String> oldProperties = oldIdentity.getProperties();
-                                       Map<String, String> newProperties = newIdentity.getProperties();
-                                       if (oldProperties.size() != newProperties.size()) {
-                                               identityListenerManager.fireIdentityUpdated(newIdentity);
-                                               continue;
-                                       }
-                                       for (Entry<String, String> oldProperty : oldProperties.entrySet()) {
-                                               if (!newProperties.containsKey(oldProperty.getKey()) || !newProperties.get(oldProperty.getKey()).equals(oldProperty.getValue())) {
-                                                       identityListenerManager.fireIdentityUpdated(newIdentity);
-                                                       break;
+                                               /* check for changes in the contexts. */
+                                               for (Identity oldIdentity : oldIdentities.get(ownIdentity).values()) {
+                                                       if (!currentIdentities.get(ownIdentity).containsKey(oldIdentity.getId())) {
+                                                               continue;
+                                                       }
+                                                       Identity newIdentity = currentIdentities.get(ownIdentity).get(oldIdentity.getId());
+                                                       Set<String> oldContexts = oldIdentity.getContexts();
+                                                       Set<String> newContexts = newIdentity.getContexts();
+                                                       if (oldContexts.size() != newContexts.size()) {
+                                                               identityListenerManager.fireIdentityUpdated(ownIdentity, newIdentity);
+                                                               continue;
+                                                       }
+                                                       for (String oldContext : oldContexts) {
+                                                               if (!newContexts.contains(oldContext)) {
+                                                                       identityListenerManager.fireIdentityUpdated(ownIdentity, newIdentity);
+                                                                       break;
+                                                               }
+                                                       }
+                                               }
+
+                                               /* check for changes in the properties. */
+                                               for (Identity oldIdentity : oldIdentities.get(ownIdentity).values()) {
+                                                       if (!currentIdentities.get(ownIdentity).containsKey(oldIdentity.getId())) {
+                                                               continue;
+                                                       }
+                                                       Identity newIdentity = currentIdentities.get(ownIdentity).get(oldIdentity.getId());
+                                                       Map<String, String> oldProperties = oldIdentity.getProperties();
+                                                       Map<String, String> newProperties = newIdentity.getProperties();
+                                                       if (oldProperties.size() != newProperties.size()) {
+                                                               identityListenerManager.fireIdentityUpdated(ownIdentity, newIdentity);
+                                                               continue;
+                                                       }
+                                                       for (Entry<String, String> oldProperty : oldProperties.entrySet()) {
+                                                               if (!newProperties.containsKey(oldProperty.getKey()) || !newProperties.get(oldProperty.getKey()).equals(oldProperty.getValue())) {
+                                                                       identityListenerManager.fireIdentityUpdated(ownIdentity, newIdentity);
+                                                                       break;
+                                                               }
+                                                       }
                                                }
                                        }
-                               }
 
-                               /* remember the current set of identities. */
-                               oldIdentities = currentIdentities;
+                                       /* remember the current set of identities. */
+                                       oldIdentities = currentIdentities;
+                               }
 
-                       } catch (PluginException pe1) {
-                               logger.log(Level.WARNING, "WoT has disappeared!", pe1);
+                       } catch (WebOfTrustException wote1) {
+                               logger.log(Level.WARNING, "WoT has disappeared!", wote1);
                        }
 
                        /* wait a minute before checking again. */
index d9ec160..26dc449 100644 (file)
 
 package net.pterodactylus.sone.freenet.wot;
 
+import java.util.Map;
+import java.util.Set;
+
 /**
- * An own identity is an identity that the owner of the node has full control
- * over.
+ * Defines a local identity, an own identity.
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
-public class OwnIdentity extends Identity {
+public interface OwnIdentity extends Identity {
+
+       /**
+        * Returns the insert URI of the identity.
+        *
+        * @return The insert URI of the identity
+        */
+       public String getInsertUri();
 
-       /** The insert URI of the identity. */
-       private final String insertUri;
+       /**
+        * Adds the given context to this identity.
+        * <p>
+        * This method is only called by the {@link IdentityManager}.
+        *
+        * @param context
+        *            The context to add
+        * @throws WebOfTrustException
+        *             if an error occurs
+        */
+       public void addContext(String context) throws WebOfTrustException;
 
        /**
-        * Creates a new own identity.
+        * Sets all contexts of this identity.
+        * <p>
+        * This method is only called by the {@link IdentityManager}.
         *
-        * @param id
-        *            The ID of the identity
-        * @param nickname
-        *            The nickname of the identity
-        * @param requestUri
-        *            The request URI of the identity
-        * @param insertUri
-        *            The insert URI of the identity
+        * @param contexts
+        *            All contexts of the identity
+        * @throws WebOfTrustException
+        *             if an error occurs
         */
-       public OwnIdentity(String id, String nickname, String requestUri, String insertUri) {
-               super(id, nickname, requestUri);
-               this.insertUri = insertUri;
-       }
+       public void setContexts(Set<String> contexts) throws WebOfTrustException;
 
-       //
-       // ACCESSORS
-       //
+       /**
+        * Removes the given context from this identity.
+        * <p>
+        * This method is only called by the {@link IdentityManager}.
+        *
+        * @param context
+        *            The context to remove
+        * @throws WebOfTrustException
+        *             if an error occurs
+        */
+       public void removeContext(String context) throws WebOfTrustException;
 
        /**
-        * Returns the insert URI of the identity.
+        * Sets the property with the given name to the given value.
         *
-        * @return The insert URI of the identity
+        * @param name
+        *            The name of the property
+        * @param value
+        *            The value of the property
+        * @throws WebOfTrustException
+        *             if an error occurs
+        */
+       public void setProperty(String name, String value) throws WebOfTrustException;
+
+       /**
+        * Sets all properties of this identity.
+        * <p>
+        * This method is only called by the {@link IdentityManager}.
+        *
+        * @param properties
+        *            The new properties of this identity
+        * @throws WebOfTrustException
+        *             if an error occurs
+        */
+       public void setProperties(Map<String, String> properties) throws WebOfTrustException;
+
+       /**
+        * Removes the property with the given name.
+        * <p>
+        * This method is only called by the {@link IdentityManager}.
+        *
+        * @param name
+        *            The name of the property to remove
+        * @throws WebOfTrustException
+        *             if an error occurs
+        */
+       public void removeProperty(String name) throws WebOfTrustException;
+
+       /**
+        * Sets the trust for the given target identity.
+        *
+        * @param target
+        *            The target to set the trust for
+        * @param trustValue
+        *            The new trust value (from {@code -100} or {@code 100})
+        * @param comment
+        *            The comment for the trust assignment
+        * @throws WebOfTrustException
+        *             if an error occurs
+        */
+       public void setTrust(Identity target, int trustValue, String comment) throws WebOfTrustException;
+
+       /**
+        * Removes any trust assignment for the given target identity.
+        *
+        * @param target
+        *            The targe to remove the trust assignment for
+        * @throws WebOfTrustException
+        *             if an error occurs
         */
-       public String getInsertUri() {
-               return insertUri;
-       }
+       public void removeTrust(Identity target) throws WebOfTrustException;
 
 }
diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/PluginConnector.java b/src/main/java/net/pterodactylus/sone/freenet/wot/PluginConnector.java
deleted file mode 100644 (file)
index 561f092..0000000
+++ /dev/null
@@ -1,203 +0,0 @@
-/*
- * Sone - PluginConnector.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.freenet.wot;
-
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-
-import net.pterodactylus.util.collection.Pair;
-import freenet.pluginmanager.FredPluginTalker;
-import freenet.pluginmanager.PluginNotFoundException;
-import freenet.pluginmanager.PluginRespirator;
-import freenet.pluginmanager.PluginTalker;
-import freenet.support.SimpleFieldSet;
-import freenet.support.api.Bucket;
-
-/**
- * Interface for talking to other plugins. Other plugins are identified by their
- * name and a unique connection identifier.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class PluginConnector implements FredPluginTalker {
-
-       /** The plugin respirator. */
-       private final PluginRespirator pluginRespirator;
-
-       /** Connector listener managers for all plugin connections. */
-       private final Map<Pair<String, String>, ConnectorListenerManager> connectorListenerManagers = Collections.synchronizedMap(new HashMap<Pair<String, String>, ConnectorListenerManager>());
-
-       /**
-        * Creates a new plugin connector.
-        *
-        * @param pluginRespirator
-        *            The plugin respirator
-        */
-       public PluginConnector(PluginRespirator pluginRespirator) {
-               this.pluginRespirator = pluginRespirator;
-       }
-
-       //
-       // LISTENER MANAGEMENT
-       //
-
-       /**
-        * Adds a connection listener for the given plugin connection.
-        *
-        * @param pluginName
-        *            The name of the plugin
-        * @param identifier
-        *            The identifier of the connection
-        * @param connectorListener
-        *            The listener to add
-        */
-       public void addConnectorListener(String pluginName, String identifier, ConnectorListener connectorListener) {
-               getConnectorListenerManager(pluginName, identifier).addListener(connectorListener);
-       }
-
-       /**
-        * Removes a connection listener for the given plugin connection.
-        *
-        * @param pluginName
-        *            The name of the plugin
-        * @param identifier
-        *            The identifier of the connection
-        * @param connectorListener
-        *            The listener to remove
-        */
-       public void removeConnectorListener(String pluginName, String identifier, ConnectorListener connectorListener) {
-               getConnectorListenerManager(pluginName, identifier).removeListener(connectorListener);
-       }
-
-       //
-       // ACTIONS
-       //
-
-       /**
-        * Sends a request to the given plugin.
-        *
-        * @param pluginName
-        *            The name of the plugin
-        * @param identifier
-        *            The identifier of the connection
-        * @param fields
-        *            The fields of the message
-        * @throws PluginException
-        *             if the plugin can not be found
-        */
-       public void sendRequest(String pluginName, String identifier, SimpleFieldSet fields) throws PluginException {
-               sendRequest(pluginName, identifier, fields, null);
-       }
-
-       /**
-        * Sends a request to the given plugin.
-        *
-        * @param pluginName
-        *            The name of the plugin
-        * @param identifier
-        *            The identifier of the connection
-        * @param fields
-        *            The fields of the message
-        * @param data
-        *            The payload of the message (may be null)
-        * @throws PluginException
-        *             if the plugin can not be found
-        */
-       public void sendRequest(String pluginName, String identifier, SimpleFieldSet fields, Bucket data) throws PluginException {
-               getPluginTalker(pluginName, identifier).send(fields, data);
-       }
-
-       //
-       // PRIVATE METHODS
-       //
-
-       /**
-        * Returns the connection listener manager for the given plugin connection,
-        * creating a new one if none does exist yet.
-        *
-        * @param pluginName
-        *            The name of the plugin
-        * @param identifier
-        *            The identifier of the connection
-        * @return The connection listener manager
-        */
-       private ConnectorListenerManager getConnectorListenerManager(String pluginName, String identifier) {
-               return getConnectorListenerManager(pluginName, identifier, true);
-       }
-
-       /**
-        * Returns the connection listener manager for the given plugin connection,
-        * optionally creating a new one if none does exist yet.
-        *
-        * @param pluginName
-        *            The name of the plugin
-        * @param identifier
-        *            The identifier of the connection
-        * @param create
-        *            {@code true} to create a new manager if there is none,
-        *            {@code false} to return {@code null} in that case
-        * @return The connection listener manager, or {@code null} if none existed
-        *         and {@code create} is {@code false}
-        */
-       private ConnectorListenerManager getConnectorListenerManager(String pluginName, String identifier, boolean create) {
-               ConnectorListenerManager connectorListenerManager = connectorListenerManagers.get(new Pair<String, String>(pluginName, identifier));
-               if (create && (connectorListenerManager == null)) {
-                       connectorListenerManager = new ConnectorListenerManager(this);
-                       connectorListenerManagers.put(new Pair<String, String>(pluginName, identifier), connectorListenerManager);
-               }
-               return connectorListenerManager;
-       }
-
-       /**
-        * Returns the plugin talker for the given plugin connection.
-        *
-        * @param pluginName
-        *            The name of the plugin
-        * @param identifier
-        *            The identifier of the connection
-        * @return The plugin talker
-        * @throws PluginException
-        *             if the plugin can not be found
-        */
-       private PluginTalker getPluginTalker(String pluginName, String identifier) throws PluginException {
-               try {
-                       return pluginRespirator.getPluginTalker(this, pluginName, identifier);
-               } catch (PluginNotFoundException pnfe1) {
-                       throw new PluginException(pnfe1);
-               }
-       }
-
-       //
-       // INTERFACE FredPluginTalker
-       //
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public void onReply(String pluginName, String identifier, SimpleFieldSet params, Bucket data) {
-               ConnectorListenerManager connectorListenerManager = getConnectorListenerManager(pluginName, identifier, false);
-               if (connectorListenerManager == null) {
-                       /* we don’t care about events for this plugin. */
-                       return;
-               }
-               connectorListenerManager.fireReceivedReply(params, data);
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/PluginException.java b/src/main/java/net/pterodactylus/sone/freenet/wot/PluginException.java
deleted file mode 100644 (file)
index 36cf49a..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Sone - PluginException.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.freenet.wot;
-
-/**
- * Exception that signals an error when communicating with a plugin.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class PluginException extends Exception {
-
-       /**
-        * Creates a new plugin exception.
-        */
-       public PluginException() {
-               super();
-       }
-
-       /**
-        * Creates a new plugin exception.
-        *
-        * @param message
-        *            The message of the exception
-        */
-       public PluginException(String message) {
-               super(message);
-       }
-
-       /**
-        * Creates a new plugin exception.
-        *
-        * @param cause
-        *            The cause of the exception
-        */
-       public PluginException(Throwable cause) {
-               super(cause);
-       }
-
-       /**
-        * Creates a new plugin exception.
-        *
-        * @param message
-        *            The message of the exception
-        * @param cause
-        *            The cause of the exception
-        */
-       public PluginException(String message, Throwable cause) {
-               super(message, cause);
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/Trust.java b/src/main/java/net/pterodactylus/sone/freenet/wot/Trust.java
new file mode 100644 (file)
index 0000000..5891c04
--- /dev/null
@@ -0,0 +1,90 @@
+/*
+ * Sone - Trust.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.freenet.wot;
+
+/**
+ * Container class for trust in the web of trust.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class Trust {
+
+       /** Explicitely assigned trust. */
+       private final Integer explicit;
+
+       /** Implicitely calculated trust. */
+       private final Integer implicit;
+
+       /** The distance from the owner of the trust tree. */
+       private final Integer distance;
+
+       /**
+        * Creates a new trust container.
+        *
+        * @param explicit
+        *            The explicit trust
+        * @param implicit
+        *            The implicit trust
+        * @param distance
+        *            The distance
+        */
+       public Trust(Integer explicit, Integer implicit, Integer distance) {
+               this.explicit = explicit;
+               this.implicit = implicit;
+               this.distance = distance;
+       }
+
+       /**
+        * Returns the trust explicitely assigned to an identity.
+        *
+        * @return The explicitely assigned trust, or {@code null} if the identity
+        *         is not in the own identity’s trust tree
+        */
+       public Integer getExplicit() {
+               return explicit;
+       }
+
+       /**
+        * Returns the implicitely assigned trust, or the calculated trust.
+        *
+        * @return The calculated trust, or {@code null} if the identity is not in
+        *         the own identity’s trust tree
+        */
+       public Integer getImplicit() {
+               return implicit;
+       }
+
+       /**
+        * Returns the distance of the trusted identity from the trusting identity.
+        *
+        * @return The distance from the own identity, or {@code null} if the
+        *         identity is not in the own identity’s trust tree
+        */
+       public Integer getDistance() {
+               return distance;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public String toString() {
+               return getClass().getName() + "[explicit=" + explicit + ",implicit=" + implicit + ",distance=" + distance + "]";
+       }
+
+}
index becb178..b6d68d3 100644 (file)
@@ -17,7 +17,6 @@
 
 package net.pterodactylus.sone.freenet.wot;
 
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
@@ -25,6 +24,9 @@ import java.util.Set;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
+import net.pterodactylus.sone.freenet.plugin.ConnectorListener;
+import net.pterodactylus.sone.freenet.plugin.PluginConnector;
+import net.pterodactylus.sone.freenet.plugin.PluginException;
 import net.pterodactylus.util.logging.Logging;
 import freenet.support.SimpleFieldSet;
 import freenet.support.api.Bucket;
@@ -45,8 +47,8 @@ public class WebOfTrustConnector implements ConnectorListener {
        /** A random connection identifier. */
        private static final String PLUGIN_CONNECTION_IDENTIFIER = "Sone-WoT-Connector-" + Math.abs(Math.random());
 
-       /** The current replies that we wait for. */
-       private final Map<String, Reply> replies = Collections.synchronizedMap(new HashMap<String, Reply>());
+       /** The current reply. */
+       private Reply reply;
 
        /** The plugin connector. */
        private final PluginConnector pluginConnector;
@@ -71,11 +73,11 @@ public class WebOfTrustConnector implements ConnectorListener {
         * Loads all own identities from the Web of Trust plugin.
         *
         * @return All own identity
-        * @throws PluginException
+        * @throws WebOfTrustException
         *             if the own identities can not be loaded
         */
-       public Set<OwnIdentity> loadAllOwnIdentities() throws PluginException {
-               Reply reply = performRequest(SimpleFieldSetConstructor.create().put("Message", "GetOwnIdentities").get(), "OwnIdentities");
+       public Set<OwnIdentity> loadAllOwnIdentities() throws WebOfTrustException {
+               Reply reply = performRequest(SimpleFieldSetConstructor.create().put("Message", "GetOwnIdentities").get());
                SimpleFieldSet fields = reply.getFields();
                int ownIdentityCounter = -1;
                Set<OwnIdentity> ownIdentities = new HashSet<OwnIdentity>();
@@ -87,9 +89,9 @@ public class WebOfTrustConnector implements ConnectorListener {
                        String requestUri = fields.get("RequestURI" + ownIdentityCounter);
                        String insertUri = fields.get("InsertURI" + ownIdentityCounter);
                        String nickname = fields.get("Nickname" + ownIdentityCounter);
-                       OwnIdentity ownIdentity = new OwnIdentity(id, nickname, requestUri, insertUri);
-                       ownIdentity.setContexts(parseContexts("Contexts" + ownIdentityCounter + ".", fields));
-                       ownIdentity.setProperties(parseProperties("Properties" + ownIdentityCounter + ".", fields));
+                       DefaultOwnIdentity ownIdentity = new DefaultOwnIdentity(this, id, nickname, requestUri, insertUri);
+                       ownIdentity.setContextsPrivate(parseContexts("Contexts" + ownIdentityCounter + ".", fields));
+                       ownIdentity.setPropertiesPrivate(parseProperties("Properties" + ownIdentityCounter + ".", fields));
                        ownIdentities.add(ownIdentity);
                }
                return ownIdentities;
@@ -122,7 +124,7 @@ public class WebOfTrustConnector implements ConnectorListener {
         *             if an error occured talking to the Web of Trust plugin
         */
        public Set<Identity> loadTrustedIdentities(OwnIdentity ownIdentity, String context) throws PluginException {
-               Reply reply = performRequest(SimpleFieldSetConstructor.create().put("Message", "GetIdentitiesByScore").put("TreeOwner", ownIdentity.getId()).put("Selection", "+").put("Context", (context == null) ? "" : context).get(), "Identities");
+               Reply reply = performRequest(SimpleFieldSetConstructor.create().put("Message", "GetIdentitiesByScore").put("TreeOwner", ownIdentity.getId()).put("Selection", "+").put("Context", (context == null) ? "" : context).get());
                SimpleFieldSet fields = reply.getFields();
                Set<Identity> identities = new HashSet<Identity>();
                int identityCounter = -1;
@@ -133,9 +135,9 @@ public class WebOfTrustConnector implements ConnectorListener {
                        }
                        String nickname = fields.get("Nickname" + identityCounter);
                        String requestUri = fields.get("RequestURI" + identityCounter);
-                       Identity identity = new Identity(id, nickname, requestUri);
-                       identity.setContexts(parseContexts("Contexts" + identityCounter + ".", fields));
-                       identity.setProperties(parseProperties("Properties" + identityCounter + ".", fields));
+                       DefaultIdentity identity = new DefaultIdentity(this, id, nickname, requestUri);
+                       identity.setContextsPrivate(parseContexts("Contexts" + identityCounter + ".", fields));
+                       identity.setPropertiesPrivate(parseProperties("Properties" + identityCounter + ".", fields));
                        identities.add(identity);
                }
                return identities;
@@ -152,7 +154,7 @@ public class WebOfTrustConnector implements ConnectorListener {
         *             if an error occured talking to the Web of Trust plugin
         */
        public void addContext(OwnIdentity ownIdentity, String context) throws PluginException {
-               performRequest(SimpleFieldSetConstructor.create().put("Message", "AddContext").put("Identity", ownIdentity.getId()).put("Context", context).get(), "ContextAdded");
+               performRequest(SimpleFieldSetConstructor.create().put("Message", "AddContext").put("Identity", ownIdentity.getId()).put("Context", context).get());
        }
 
        /**
@@ -166,7 +168,7 @@ public class WebOfTrustConnector implements ConnectorListener {
         *             if an error occured talking to the Web of Trust plugin
         */
        public void removeContext(OwnIdentity ownIdentity, String context) throws PluginException {
-               performRequest(SimpleFieldSetConstructor.create().put("Message", "RemoveContext").put("Identity", ownIdentity.getId()).put("Context", context).get(), "ContextRemoved");
+               performRequest(SimpleFieldSetConstructor.create().put("Message", "RemoveContext").put("Identity", ownIdentity.getId()).put("Context", context).get());
        }
 
        /**
@@ -181,7 +183,7 @@ public class WebOfTrustConnector implements ConnectorListener {
         *             if an error occured talking to the Web of Trust plugin
         */
        public String getProperty(Identity identity, String name) throws PluginException {
-               Reply reply = performRequest(SimpleFieldSetConstructor.create().put("Message", "GetProperty").put("Identity", identity.getId()).put("Property", name).get(), "PropertyValue");
+               Reply reply = performRequest(SimpleFieldSetConstructor.create().put("Message", "GetProperty").put("Identity", identity.getId()).put("Property", name).get());
                return reply.getFields().get("Property");
        }
 
@@ -198,7 +200,7 @@ public class WebOfTrustConnector implements ConnectorListener {
         *             if an error occured talking to the Web of Trust plugin
         */
        public void setProperty(OwnIdentity ownIdentity, String name, String value) throws PluginException {
-               performRequest(SimpleFieldSetConstructor.create().put("Message", "SetProperty").put("Identity", ownIdentity.getId()).put("Property", name).put("Value", value).get(), "PropertyAdded");
+               performRequest(SimpleFieldSetConstructor.create().put("Message", "SetProperty").put("Identity", ownIdentity.getId()).put("Property", name).put("Value", value).get());
        }
 
        /**
@@ -212,7 +214,74 @@ public class WebOfTrustConnector implements ConnectorListener {
         *             if an error occured talking to the Web of Trust plugin
         */
        public void removeProperty(OwnIdentity ownIdentity, String name) throws PluginException {
-               performRequest(SimpleFieldSetConstructor.create().put("Message", "RemoveProperty").put("Identity", ownIdentity.getId()).put("Property", name).get(), "PropertyRemoved");
+               performRequest(SimpleFieldSetConstructor.create().put("Message", "RemoveProperty").put("Identity", ownIdentity.getId()).put("Property", name).get());
+       }
+
+       /**
+        * Returns the trust for the given identity assigned to it by the given own
+        * identity.
+        *
+        * @param ownIdentity
+        *            The own identity
+        * @param identity
+        *            The identity to get the trust for
+        * @return The trust for the given identity
+        * @throws PluginException
+        *             if an error occured talking to the Web of Trust plugin
+        */
+       public Trust getTrust(OwnIdentity ownIdentity, Identity identity) throws PluginException {
+               Reply getTrustReply = performRequest(SimpleFieldSetConstructor.create().put("Message", "GetIdentity").put("TreeOwner", ownIdentity.getId()).put("Identity", identity.getId()).get());
+               String trust = getTrustReply.getFields().get("Trust");
+               String score = getTrustReply.getFields().get("Score");
+               String rank = getTrustReply.getFields().get("Rank");
+               Integer explicit = null;
+               Integer implicit = null;
+               Integer distance = null;
+               try {
+                       explicit = Integer.valueOf(trust);
+               } catch (NumberFormatException nfe1) {
+                       /* ignore. */
+               }
+               try {
+                       implicit = Integer.valueOf(score);
+                       distance = Integer.valueOf(rank);
+               } catch (NumberFormatException nfe1) {
+                       /* ignore. */
+               }
+               return new Trust(explicit, implicit, distance);
+       }
+
+       /**
+        * Sets the trust for the given identity.
+        *
+        * @param ownIdentity
+        *            The trusting identity
+        * @param identity
+        *            The trusted identity
+        * @param trust
+        *            The amount of trust (-100 thru 100)
+        * @param comment
+        *            The comment or explanation of the trust value
+        * @throws PluginException
+        *             if an error occured talking to the Web of Trust plugin
+        */
+       public void setTrust(OwnIdentity ownIdentity, Identity identity, int trust, String comment) throws PluginException {
+               performRequest(SimpleFieldSetConstructor.create().put("Message", "SetTrust").put("Truster", ownIdentity.getId()).put("Trustee", identity.getId()).put("Value", String.valueOf(trust)).put("Comment", comment).get());
+       }
+
+       /**
+        * Removes any trust assignment of the given own identity for the given
+        * identity.
+        *
+        * @param ownIdentity
+        *            The own identity
+        * @param identity
+        *            The identity to remove all trust for
+        * @throws WebOfTrustException
+        *             if an error occurs
+        */
+       public void removeTrust(OwnIdentity ownIdentity, Identity identity) throws WebOfTrustException {
+               performRequest(SimpleFieldSetConstructor.create().put("Message", "RemoveTrust").put("Truster", ownIdentity.getId()).put("Trustee", identity.getId()).get());
        }
 
        /**
@@ -223,7 +292,7 @@ public class WebOfTrustConnector implements ConnectorListener {
         *             if the plugin is not loaded
         */
        public void ping() throws PluginException {
-               performRequest(SimpleFieldSetConstructor.create().put("Message", "Ping").get(), "Pong");
+               performRequest(SimpleFieldSetConstructor.create().put("Message", "Ping").get());
        }
 
        //
@@ -281,14 +350,12 @@ public class WebOfTrustConnector implements ConnectorListener {
         *
         * @param fields
         *            The fields of the message
-        * @param targetMessages
-        *            The messages of the reply to wait for
         * @return The reply message
         * @throws PluginException
         *             if the request could not be sent
         */
-       private Reply performRequest(SimpleFieldSet fields, String... targetMessages) throws PluginException {
-               return performRequest(fields, null, targetMessages);
+       private Reply performRequest(SimpleFieldSet fields) throws PluginException {
+               return performRequest(fields, null);
        }
 
        /**
@@ -299,43 +366,24 @@ public class WebOfTrustConnector implements ConnectorListener {
         *            The fields of the message
         * @param data
         *            The payload of the message
-        * @param targetMessages
-        *            The messages of the reply to wait for
         * @return The reply message
         * @throws PluginException
         *             if the request could not be sent
         */
-       private Reply performRequest(SimpleFieldSet fields, Bucket data, String... targetMessages) throws PluginException {
-               @SuppressWarnings("synthetic-access")
-               Reply reply = new Reply();
-               for (String targetMessage : targetMessages) {
-                       replies.put(targetMessage, reply);
-               }
-               replies.put("Error", reply);
+       private synchronized Reply performRequest(SimpleFieldSet fields, Bucket data) throws PluginException {
+               reply = new Reply();
+               logger.log(Level.FINE, "Sending FCP Request: " + fields.get("Message"));
                synchronized (reply) {
                        pluginConnector.sendRequest(WOT_PLUGIN_NAME, PLUGIN_CONNECTION_IDENTIFIER, fields, data);
                        try {
-                               long now = System.currentTimeMillis();
-                               while ((reply.getFields() == null) && ((System.currentTimeMillis() - now) < 60000)) {
-                                       reply.wait(60000 - (System.currentTimeMillis() - now));
-                               }
-                               if (reply.getFields() == null) {
-                                       for (String targetMessage : targetMessages) {
-                                               replies.remove(targetMessage);
-                                       }
-                                       replies.remove("Error");
-                                       throw new PluginException("Timeout waiting for " + targetMessages[0] + "!");
-                               }
+                               reply.wait();
                        } catch (InterruptedException ie1) {
-                               logger.log(Level.WARNING, "Got interrupted while waiting for reply on " + targetMessages[0] + ".", ie1);
+                               logger.log(Level.WARNING, "Got interrupted while waiting for reply on " + fields.get("Message") + ".", ie1);
                        }
                }
-               for (String targetMessage : targetMessages) {
-                       replies.remove(targetMessage);
-               }
-               replies.remove("Error");
-               if ((reply.getFields() != null) && reply.getFields().get("Message").equals("Error")) {
-                       throw new PluginException("Could not perform request for " + targetMessages[0]);
+               logger.log(Level.FINEST, "Received FCP Response for %s: %s", new Object[] { fields.get("Message"), (reply.getFields() != null) ? reply.getFields().get("Message") : null });
+               if ((reply.getFields() == null) || "Error".equals(reply.getFields().get("Message"))) {
+                       throw new PluginException("Could not perform request for " + fields.get("Message"));
                }
                return reply;
        }
@@ -351,11 +399,6 @@ public class WebOfTrustConnector implements ConnectorListener {
        public void receivedReply(PluginConnector pluginConnector, SimpleFieldSet fields, Bucket data) {
                String messageName = fields.get("Message");
                logger.log(Level.FINEST, "Received Reply from Plugin: " + messageName);
-               Reply reply = replies.remove(messageName);
-               if (reply == null) {
-                       logger.log(Level.FINE, "Not waiting for a “%s” message.", messageName);
-                       return;
-               }
                synchronized (reply) {
                        reply.setFields(fields);
                        reply.setData(data);
@@ -376,6 +419,11 @@ public class WebOfTrustConnector implements ConnectorListener {
                /** The payload of the reply. */
                private Bucket data;
 
+               /** Empty constructor. */
+               public Reply() {
+                       /* do nothing. */
+               }
+
                /**
                 * Returns the fields of the reply.
                 *
diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/WebOfTrustException.java b/src/main/java/net/pterodactylus/sone/freenet/wot/WebOfTrustException.java
new file mode 100644 (file)
index 0000000..f59b2a3
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+ * Sone - WebOfTrustException.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.freenet.wot;
+
+/**
+ * Exception that signals an error processing web of trust identities, mostly
+ * when communicating with the web of trust plugin.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class WebOfTrustException extends Exception {
+
+       /**
+        * Creates a new web of trust exception.
+        */
+       public WebOfTrustException() {
+               super();
+       }
+
+       /**
+        * Creates a new web of trust exception.
+        *
+        * @param message
+        *            The message of the exception
+        */
+       public WebOfTrustException(String message) {
+               super(message);
+       }
+
+       /**
+        * Creates a new web of trust exception.
+        *
+        * @param cause
+        *            The cause of the exception
+        */
+       public WebOfTrustException(Throwable cause) {
+               super(cause);
+       }
+
+       /**
+        * Creates a new web of trust exception.
+        *
+        * @param message
+        *            The message of the exception
+        * @param cause
+        *            The cause of the exception
+        */
+       public WebOfTrustException(String message, Throwable cause) {
+               super(message, cause);
+       }
+
+}
index 219f989..0f7f0e8 100644 (file)
@@ -25,8 +25,8 @@ import java.util.logging.Logger;
 import net.pterodactylus.sone.core.Core;
 import net.pterodactylus.sone.core.FreenetInterface;
 import net.pterodactylus.sone.freenet.PluginStoreConfigurationBackend;
+import net.pterodactylus.sone.freenet.plugin.PluginConnector;
 import net.pterodactylus.sone.freenet.wot.IdentityManager;
-import net.pterodactylus.sone.freenet.wot.PluginConnector;
 import net.pterodactylus.sone.freenet.wot.WebOfTrustConnector;
 import net.pterodactylus.sone.web.WebInterface;
 import net.pterodactylus.util.config.Configuration;
@@ -78,7 +78,7 @@ public class SonePlugin implements FredPlugin, FredPluginL10n, FredPluginBaseL10
        }
 
        /** The version. */
-       public static final Version VERSION = new Version(0, 3, 6, 5);
+       public static final Version VERSION = new Version(0, 5, 1);
 
        /** The logger. */
        private static final Logger logger = Logging.getLogger(SonePlugin.class);
index 7d3e3c7..c7e5b7c 100644 (file)
@@ -48,8 +48,7 @@ public class ListNotification<T> extends TemplateNotification {
         */
        public ListNotification(String id, String key, Template template) {
                super(id, template);
-               template.set(key, elements);
-               template.set("notification", this);
+               template.getInitialContext().set(key, elements);
        }
 
        //
index e2c6f16..5042995 100644 (file)
@@ -24,8 +24,8 @@ import java.util.Map;
 
 import net.pterodactylus.sone.data.Album;
 import net.pterodactylus.util.template.Accessor;
-import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.ReflectionAccessor;
+import net.pterodactylus.util.template.TemplateContext;
 
 /**
  * {@link Accessor} implementation for {@link Album}s. A property named
@@ -40,7 +40,7 @@ public class AlbumAccessor extends ReflectionAccessor {
         * {@inheritDoc}
         */
        @Override
-       public Object get(DataProvider dataProvider, Object object, String member) {
+       public Object get(TemplateContext templateContext, Object object, String member) {
                Album album = (Album) object;
                if ("backlinks".equals(member)) {
                        List<Map<String, String>> backlinks = new ArrayList<Map<String, String>>();
@@ -52,7 +52,7 @@ public class AlbumAccessor extends ReflectionAccessor {
                        backlinks.add(0, createLink("viewSone.html?sone=" + album.getSone().getId(), SoneAccessor.getNiceName(album.getSone())));
                        return backlinks;
                }
-               return super.get(dataProvider, object, member);
+               return super.get(templateContext, object, member);
        }
 
        //
index 10e3ea3..0fa9129 100644 (file)
@@ -24,8 +24,8 @@ import java.util.List;
 
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.util.template.Accessor;
-import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.ReflectionAccessor;
+import net.pterodactylus.util.template.TemplateContext;
 
 /**
  * {@link Accessor} for {@link Collection}s that adds a couple of specialized
@@ -44,7 +44,7 @@ public class CollectionAccessor extends ReflectionAccessor {
         * {@inheritDoc}
         */
        @Override
-       public Object get(DataProvider dataProvider, Object object, String member) {
+       public Object get(TemplateContext templateContext, Object object, String member) {
                if (object == null) {
                        return null;
                }
@@ -67,7 +67,7 @@ public class CollectionAccessor extends ReflectionAccessor {
                        }
                        return soneNames.toString();
                }
-               return super.get(dataProvider, object, member);
+               return super.get(templateContext, object, member);
        }
 
 }
index 6ba5d6b..611c6d1 100644 (file)
@@ -19,8 +19,8 @@ package net.pterodactylus.sone.template;
 
 import java.util.Map;
 
-import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.Filter;
+import net.pterodactylus.util.template.TemplateContext;
 
 /**
  * Converts the {@link String} {@link String#valueOf(Object) representation} of
@@ -35,7 +35,7 @@ public class CssClassNameFilter implements Filter {
         * {@inheritDoc}
         */
        @Override
-       public Object format(DataProvider dataProvider, Object data, Map<String, String> parameters) {
+       public Object format(TemplateContext templateContext, Object data, Map<String, String> parameters) {
                return String.valueOf(data).replaceAll("[^a-zA-Z0-9-]", "_");
        }
 
index c57932e..a25d619 100644 (file)
@@ -20,12 +20,12 @@ package net.pterodactylus.sone.template;
 import java.util.Map;
 
 import net.pterodactylus.sone.web.page.Page.Request;
-import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.Plugin;
+import net.pterodactylus.util.template.TemplateContext;
 
 /**
- * Extracts a page number from a {@link Request}’s parameters and stores it in a
- * {@link DataProvider}.
+ * Extracts a page number from a {@link Request}’s parameters and stores it in
+ * the {@link TemplateContext}.
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
@@ -35,7 +35,7 @@ public class GetPagePlugin implements Plugin {
         * {@inheritDoc}
         */
        @Override
-       public void execute(DataProvider dataProvider, Map<String, String> parameters) {
+       public void execute(TemplateContext templateContext, Map<String, String> parameters) {
                String requestKey = parameters.get("request");
                String parameter = parameters.get("parameter");
                String pageKey = parameters.get("key");
@@ -50,7 +50,7 @@ public class GetPagePlugin implements Plugin {
                        pageKey = "page";
                }
 
-               Request request = (Request) dataProvider.getData(requestKey);
+               Request request = (Request) templateContext.get(requestKey);
                String pageString = request.getHttpRequest().getParam(parameter);
                int page = 0;
                try {
@@ -58,7 +58,7 @@ public class GetPagePlugin implements Plugin {
                } catch (NumberFormatException nfe1) {
                        /* ignore. */
                }
-               dataProvider.setData(pageKey, page);
+               templateContext.set(pageKey, page);
        }
 
 }
index 84989b3..eec9347 100644 (file)
@@ -23,8 +23,8 @@ import net.pterodactylus.sone.core.Core;
 import net.pterodactylus.sone.freenet.wot.Identity;
 import net.pterodactylus.sone.freenet.wot.OwnIdentity;
 import net.pterodactylus.util.template.Accessor;
-import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.ReflectionAccessor;
+import net.pterodactylus.util.template.TemplateContext;
 
 /**
  * {@link Accessor} implementation that adds a “uniqueNickname” member to an
@@ -51,7 +51,7 @@ public class IdentityAccessor extends ReflectionAccessor {
         * {@inheritDoc}
         */
        @Override
-       public Object get(DataProvider dataProvider, Object object, String member) {
+       public Object get(TemplateContext templateContext, Object object, String member) {
                Identity identity = (Identity) object;
                if ("uniqueNickname".equals(member)) {
                        int minLength = -1;
@@ -75,7 +75,7 @@ public class IdentityAccessor extends ReflectionAccessor {
                        } while (!found && (minLength < 43));
                        return getAbbreviatedNickname(identity, minLength);
                }
-               return super.get(dataProvider, object, member);
+               return super.get(templateContext, object, member);
        }
 
        //
diff --git a/src/main/java/net/pterodactylus/sone/template/JavascriptFilter.java b/src/main/java/net/pterodactylus/sone/template/JavascriptFilter.java
new file mode 100644 (file)
index 0000000..f852202
--- /dev/null
@@ -0,0 +1,68 @@
+/*
+ * Sone - JavascriptFilter.java - Copyright © 2011 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.template;
+
+import java.util.Map;
+
+import net.pterodactylus.util.number.Hex;
+import net.pterodactylus.util.template.Filter;
+import net.pterodactylus.util.template.TemplateContext;
+
+/**
+ * Escapes double quotes, backslashes, carriage returns and line feeds, and
+ * additionally encloses a given string with double quotes to make it possible
+ * to use a string in Javascript.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class JavascriptFilter implements Filter {
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public Object format(TemplateContext templateContext, Object data, Map<String, String> parameters) {
+               StringBuilder javascriptString = new StringBuilder();
+               javascriptString.append('"');
+               for (char c : String.valueOf(data).toCharArray()) {
+                       if (c == '\r') {
+                               javascriptString.append("\\r");
+                               continue;
+                       }
+                       if (c == '\n') {
+                               javascriptString.append("\\n");
+                               continue;
+                       }
+                       if (c == '\t') {
+                               javascriptString.append("\\t");
+                               continue;
+                       }
+                       if ((c == '"') || (c == '\\')) {
+                               javascriptString.append('\\');
+                               javascriptString.append(c);
+                       } else if (c < 32) {
+                               javascriptString.append("\\x").append(Hex.toHex((byte) c));
+                       } else {
+                               javascriptString.append(c);
+                       }
+               }
+               javascriptString.append('"');
+               return javascriptString.toString();
+       }
+
+}
index 1c27b29..a8f34a8 100644 (file)
@@ -23,8 +23,8 @@ import java.util.List;
 
 import net.pterodactylus.util.notify.Notification;
 import net.pterodactylus.util.notify.NotificationManager;
-import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.ReflectionAccessor;
+import net.pterodactylus.util.template.TemplateContext;
 
 /**
  * Adds additional properties to a {@link NotificationManager}.
@@ -44,7 +44,7 @@ public class NotificationManagerAccessor extends ReflectionAccessor {
         * {@inheritDoc}
         */
        @Override
-       public Object get(DataProvider dataProvider, Object object, String member) {
+       public Object get(TemplateContext templateContext, Object object, String member) {
                NotificationManager notificationManager = (NotificationManager) object;
                if ("all".equals(member)) {
                        List<Notification> notifications = new ArrayList<Notification>(notificationManager.getNotifications());
@@ -55,7 +55,7 @@ public class NotificationManagerAccessor extends ReflectionAccessor {
                        Collections.sort(notifications, Notification.LAST_UPDATED_TIME_SORTER);
                        return notifications;
                }
-               return super.get(dataProvider, object, member);
+               return super.get(templateContext, object, member);
        }
 
 }
diff --git a/src/main/java/net/pterodactylus/sone/template/ParserFilter.java b/src/main/java/net/pterodactylus/sone/template/ParserFilter.java
new file mode 100644 (file)
index 0000000..1e274cc
--- /dev/null
@@ -0,0 +1,82 @@
+/*
+ * Sone - ParserFilter.java - Copyright © 2011 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.template;
+
+import java.io.IOException;
+import java.io.StringReader;
+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.util.template.Filter;
+import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.template.TemplateContextFactory;
+
+/**
+ * Filter that filters a given text through a {@link FreenetLinkParser}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class ParserFilter implements Filter {
+
+       /** The core. */
+       private final Core core;
+
+       /** The link parser. */
+       private final FreenetLinkParser linkParser;
+
+       /**
+        * Creates a new filter that runs its input through a
+        * {@link FreenetLinkParser}.
+        *
+        * @param core
+        *            The core
+        * @param templateContextFactory
+        *            The context factory for rendering the parts
+        */
+       public ParserFilter(Core core, TemplateContextFactory templateContextFactory) {
+               this.core = core;
+               linkParser = new FreenetLinkParser(templateContextFactory);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public Object format(TemplateContext templateContext, Object data, Map<String, String> parameters) {
+               String text = String.valueOf(data);
+               String soneKey = parameters.get("sone");
+               if (soneKey == null) {
+                       soneKey = "sone";
+               }
+               Sone sone = (Sone) templateContext.get(soneKey);
+               if (sone == null) {
+                       sone = core.getSone(soneKey, false);
+               }
+               FreenetLinkParserContext context = new FreenetLinkParserContext(sone);
+               try {
+                       return linkParser.parse(context, new StringReader(text));
+               } catch (IOException ioe1) {
+                       /* no exceptions in a StringReader, ignore. */
+               }
+               return null;
+       }
+
+}
index 95dc614..ca0c84a 100644 (file)
 
 package net.pterodactylus.sone.template;
 
-import java.io.IOException;
-import java.io.StringReader;
-
 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;
+import net.pterodactylus.util.template.TemplateContext;
 
 /**
  * Accessor for {@link Post} objects that adds additional properties:
@@ -40,9 +34,6 @@ import net.pterodactylus.util.template.TemplateFactory;
  */
 public class PostAccessor extends ReflectionAccessor {
 
-       /** Parser for Freenet links. */
-       private final FreenetLinkParser linkParser;
-
        /** The core to get the replies from. */
        private final Core core;
 
@@ -51,41 +42,32 @@ public class PostAccessor extends ReflectionAccessor {
         *
         * @param core
         *            The core to get the replies from
-        * @param templateFactory
-        *            The template factory for the text parser
         */
-       public PostAccessor(Core core, TemplateFactory templateFactory) {
+       public PostAccessor(Core core) {
                this.core = core;
-               linkParser = new FreenetLinkParser(templateFactory);
        }
 
        /**
         * {@inheritDoc}
         */
        @Override
-       public Object get(DataProvider dataProvider, Object object, String member) {
+       public Object get(TemplateContext templateContext, Object object, String member) {
                Post post = (Post) object;
                if ("replies".equals(member)) {
                        return core.getReplies(post);
                } else if (member.equals("likes")) {
                        return core.getLikes(post);
                } else if (member.equals("liked")) {
-                       Sone currentSone = (Sone) dataProvider.getData("currentSone");
+                       Sone currentSone = (Sone) templateContext.get("currentSone");
                        return (currentSone != null) && (currentSone.isLikedPostId(post.getId()));
                } else if (member.equals("new")) {
-                       return core.isNewPost(post.getId(), false);
-               } else if (member.equals("text")) {
-                       String text = post.getText();
-                       if (text == null) {
-                               return null;
-                       }
-                       try {
-                               return linkParser.parse(new FreenetLinkParserContext(post.getSone()), new StringReader(text));
-                       } catch (IOException ioe1) {
-                               /* ignore. */
-                       }
+                       return core.isNewPost(post.getId());
+               } else if (member.equals("bookmarked")) {
+                       return core.isBookmarked(post);
+               } else if (member.equals("loaded")) {
+                       return post.getSone() != null;
                }
-               return super.get(dataProvider, object, member);
+               return super.get(templateContext, object, member);
        }
 
 }
index db6c528..1e54d53 100644 (file)
 
 package net.pterodactylus.sone.template;
 
-import java.io.IOException;
-import java.io.StringReader;
-
 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;
-import net.pterodactylus.util.template.TemplateFactory;
+import net.pterodactylus.util.template.TemplateContext;
 
 /**
  * {@link Accessor} implementation that adds a couple of properties to
@@ -38,9 +32,6 @@ import net.pterodactylus.util.template.TemplateFactory;
  */
 public class ReplyAccessor extends ReflectionAccessor {
 
-       /** Parser for Freenet links. */
-       private final FreenetLinkParser linkParser;
-
        /** The core. */
        private final Core core;
 
@@ -49,36 +40,28 @@ public class ReplyAccessor extends ReflectionAccessor {
         *
         * @param core
         *            The core
-        * @param templateFactory
-        *            The template factory for the text parser
         */
-       public ReplyAccessor(Core core, TemplateFactory templateFactory) {
+       public ReplyAccessor(Core core) {
                this.core = core;
-               linkParser = new FreenetLinkParser(templateFactory);
        }
 
        /**
         * {@inheritDoc}
         */
        @Override
-       public Object get(DataProvider dataProvider, Object object, String member) {
+       public Object get(TemplateContext templateContext, Object object, String member) {
                Reply reply = (Reply) object;
                if ("likes".equals(member)) {
                        return core.getLikes(reply);
                } else if (member.equals("liked")) {
-                       Sone currentSone = (Sone) dataProvider.getData("currentSone");
+                       Sone currentSone = (Sone) templateContext.get("currentSone");
                        return (currentSone != null) && (currentSone.isLikedReplyId(reply.getId()));
                } else if (member.equals("new")) {
-                       return core.isNewReply(reply.getId(), false);
-               } else if (member.equals("text")) {
-                       String text = reply.getText();
-                       try {
-                               return linkParser.parse(new FreenetLinkParserContext(reply.getSone()), new StringReader(text));
-                       } catch (IOException ioe1) {
-                               /* ignore. */
-                       }
+                       return core.isNewReply(reply.getId());
+               } else if (member.equals("loaded")) {
+                       return reply.getSone() != null;
                }
-               return super.get(dataProvider, object, member);
+               return super.get(templateContext, object, member);
        }
 
 }
index 380027b..fb31719 100644 (file)
@@ -27,8 +27,8 @@ import java.util.Map;
 import java.util.Map.Entry;
 
 import net.pterodactylus.sone.web.page.Page.Request;
-import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.Filter;
+import net.pterodactylus.util.template.TemplateContext;
 
 /**
  * This filter expects a {@link Request} as input and outputs a {@link URI} that
@@ -44,17 +44,17 @@ public class RequestChangeFilter implements Filter {
         * {@inheritDoc}
         */
        @Override
-       public Object format(DataProvider dataProvider, Object data, Map<String, String> parameters) {
+       public Object format(TemplateContext templateContext, Object data, Map<String, String> parameters) {
                Request request = (Request) data;
                String name = parameters.get("name");
                String nameKey = parameters.get("nameKey");
                if (nameKey != null) {
-                       name = String.valueOf(dataProvider.getData(nameKey));
+                       name = String.valueOf(templateContext.get(nameKey));
                }
                String key = parameters.get("key");
                String value = null;
                if (key != null) {
-                       value = String.valueOf(dataProvider.getData(key));
+                       value = String.valueOf(templateContext.get(key));
                }
                if (value == null) {
                        value = parameters.get("value");
index c9c5f48..23b2227 100644 (file)
 
 package net.pterodactylus.sone.template;
 
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
 import net.pterodactylus.sone.core.Core;
 import net.pterodactylus.sone.core.Core.SoneStatus;
 import net.pterodactylus.sone.data.Profile;
 import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.freenet.wot.Trust;
+import net.pterodactylus.util.logging.Logging;
 import net.pterodactylus.util.template.Accessor;
-import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.ReflectionAccessor;
+import net.pterodactylus.util.template.TemplateContext;
 
 /**
  * {@link Accessor} for {@link Sone}s that adds a couple of properties to Sones.
@@ -34,7 +39,7 @@ import net.pterodactylus.util.template.ReflectionAccessor;
  * <dt>friend</dt>
  * <dd>Will return {@code true} if the sone in question is a friend of the
  * currently logged in Sone (as determined by accessing the “currentSone”
- * variable of the given {@link DataProvider}).</dd>
+ * variable of the given {@link TemplateContext}).</dd>
  * <dt>current</dt>
  * <dd>Will return {@code true} if the sone in question is the currently logged
  * in Sone.</dd>
@@ -44,6 +49,9 @@ import net.pterodactylus.util.template.ReflectionAccessor;
  */
 public class SoneAccessor extends ReflectionAccessor {
 
+       /** The logger. */
+       private static final Logger logger = Logging.getLogger(SoneAccessor.class);
+
        /** The core. */
        private final Core core;
 
@@ -61,17 +69,17 @@ public class SoneAccessor extends ReflectionAccessor {
         * {@inheritDoc}
         */
        @Override
-       public Object get(DataProvider dataProvider, Object object, String member) {
+       public Object get(TemplateContext templateContext, Object object, String member) {
                Sone sone = (Sone) object;
                if (member.equals("niceName")) {
                        return getNiceName(sone);
                } else if (member.equals("local")) {
-                       return sone.getInsertUri() != null;
+                       return core.isLocalSone(sone);
                } else if (member.equals("friend")) {
-                       Sone currentSone = (Sone) dataProvider.getData("currentSone");
+                       Sone currentSone = (Sone) templateContext.get("currentSone");
                        return (currentSone != null) && currentSone.hasFriend(sone.getId());
                } else if (member.equals("current")) {
-                       Sone currentSone = (Sone) dataProvider.getData("currentSone");
+                       Sone currentSone = (Sone) templateContext.get("currentSone");
                        return (currentSone != null) && currentSone.equals(sone);
                } else if (member.equals("modified")) {
                        return core.isModifiedSone(sone);
@@ -86,11 +94,22 @@ public class SoneAccessor extends ReflectionAccessor {
                } else if (member.equals("downloading")) {
                        return core.getSoneStatus(sone) == SoneStatus.downloading;
                } else if (member.equals("new")) {
-                       return core.isNewSone(sone);
+                       return core.isNewSone(sone.getId());
                } else if (member.equals("locked")) {
                        return core.isLocked(sone);
+               } else if (member.equals("trust")) {
+                       Sone currentSone = (Sone) templateContext.get("currentSone");
+                       if (currentSone == null) {
+                               return null;
+                       }
+                       Trust trust = core.getTrust(currentSone, sone);
+                       logger.log(Level.FINEST, "Trust for %s by %s: %s", new Object[] { sone, currentSone, trust });
+                       if (trust == null) {
+                               return new Trust(null, null, null);
+                       }
+                       return trust;
                }
-               return super.get(dataProvider, object, member);
+               return super.get(templateContext, object, member);
        }
 
        //
index f0075e4..857017c 100644 (file)
@@ -19,8 +19,8 @@ package net.pterodactylus.sone.template;
 
 import java.util.Map;
 
-import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.Filter;
+import net.pterodactylus.util.template.TemplateContext;
 
 /**
  * {@link Filter} implementation that executes
@@ -37,7 +37,7 @@ public class SubstringFilter implements Filter {
         * {@inheritDoc}
         */
        @Override
-       public Object format(DataProvider dataProvider, Object data, Map<String, String> parameters) {
+       public Object format(TemplateContext templateContext, Object data, Map<String, String> parameters) {
                String startString = parameters.get("start");
                String lengthString = parameters.get("length");
                int start = 0;
diff --git a/src/main/java/net/pterodactylus/sone/template/TrustAccessor.java b/src/main/java/net/pterodactylus/sone/template/TrustAccessor.java
new file mode 100644 (file)
index 0000000..4aad3c9
--- /dev/null
@@ -0,0 +1,54 @@
+/*
+ * Sone - TrustAccessor.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.template;
+
+import net.pterodactylus.sone.freenet.wot.Trust;
+import net.pterodactylus.util.template.Accessor;
+import net.pterodactylus.util.template.ReflectionAccessor;
+import net.pterodactylus.util.template.TemplateContext;
+
+/**
+ * {@link Accessor} implementation for {@link Trust} values, adding the
+ * following properties:
+ * <dl>
+ * <dt>assigned</dt>
+ * <dd>{@link Boolean} that indicates whether this trust relationship has an
+ * explicit value assigned to it.</dd>
+ * </dl>
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class TrustAccessor extends ReflectionAccessor {
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public Object get(TemplateContext templateContext, Object object, String member) {
+               Trust trust = (Trust) object;
+               if ("assigned".equals(member)) {
+                       return trust.getExplicit() != null;
+               } else if ("maximum".equals(member)) {
+                       return ((trust.getExplicit() != null) && (trust.getExplicit() >= 100)) || ((trust.getImplicit() != null) && (trust.getImplicit() >= 100));
+               } else if ("hasDistance".equals(member)) {
+                       return (trust.getDistance() != null) && (trust.getDistance() != Integer.MAX_VALUE);
+               }
+               return super.get(templateContext, object, member);
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/template/UnknownDateFilter.java b/src/main/java/net/pterodactylus/sone/template/UnknownDateFilter.java
new file mode 100644 (file)
index 0000000..586e90c
--- /dev/null
@@ -0,0 +1,66 @@
+/*
+ * Sone - UnknownDateFilter.java - Copyright © 2011 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.template;
+
+import java.util.Map;
+
+import net.pterodactylus.util.template.Filter;
+import net.pterodactylus.util.template.TemplateContext;
+import freenet.l10n.BaseL10n;
+
+/**
+ * {@link Filter} implementation that replaces a {@link Long} with a value of
+ * {@code 0} by a {@link String} from an {@link BaseL10n l10n handler}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class UnknownDateFilter implements Filter {
+
+       /** The l10n handler. */
+       private BaseL10n l10nHandler;
+
+       /** The key for the text to show. */
+       private final String unknownKey;
+
+       /**
+        * Creates a new unknown date filter.
+        *
+        * @param l10nHandler
+        *            The l10n handler
+        * @param unknownKey
+        *            The key of the text to show
+        */
+       public UnknownDateFilter(BaseL10n l10nHandler, String unknownKey) {
+               this.l10nHandler = l10nHandler;
+               this.unknownKey = unknownKey;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public Object format(TemplateContext templateContext, Object data, Map<String, String> parameters) {
+               if (data instanceof Long) {
+                       if ((Long) data == 0) {
+                               return l10nHandler.getString(unknownKey);
+                       }
+               }
+               return data;
+       }
+
+}
index 63f68b8..704792c 100644 (file)
@@ -28,7 +28,8 @@ import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 import net.pterodactylus.util.logging.Logging;
-import net.pterodactylus.util.template.TemplateFactory;
+import net.pterodactylus.util.template.TemplateContextFactory;
+import net.pterodactylus.util.template.TemplateParser;
 import freenet.keys.FreenetURI;
 
 /**
@@ -72,16 +73,16 @@ public class FreenetLinkParser implements Parser<FreenetLinkParserContext> {
        }
 
        /** The template factory. */
-       private final TemplateFactory templateFactory;
+       private final TemplateContextFactory templateContextFactory;
 
        /**
         * Creates a new freenet link parser.
         *
-        * @param templateFactory
-        *            The template factory
+        * @param templateContextFactory
+        *            The template context factory
         */
-       public FreenetLinkParser(TemplateFactory templateFactory) {
-               this.templateFactory = templateFactory;
+       public FreenetLinkParser(TemplateContextFactory templateContextFactory) {
+               this.templateContextFactory = templateContextFactory;
        }
 
        //
@@ -147,36 +148,35 @@ public class FreenetLinkParser implements Parser<FreenetLinkParserContext> {
                                        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) {
-                                               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 ((linkType == LinkType.KSK) || (linkType == LinkType.CHK) || (linkType == LinkType.SSK) || (linkType == LinkType.USK)) {
+                                               FreenetURI uri;
                                                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));
+                                               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()));
                                                        }
-                                               } else {
+                                                       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);
@@ -219,7 +219,7 @@ public class FreenetLinkParser implements Parser<FreenetLinkParserContext> {
         * @return The part that displays the given text
         */
        private Part createPlainTextPart(String text) {
-               return new TemplatePart(templateFactory.createTemplate(new StringReader("<% text|html>"))).set("text", text);
+               return new TemplatePart(templateContextFactory, TemplateParser.parse(new StringReader("<% text|html>"))).set("text", text);
        }
 
        /**
@@ -233,7 +233,7 @@ public class FreenetLinkParser implements Parser<FreenetLinkParserContext> {
         * @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);
+               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);
        }
 
        /**
@@ -247,7 +247,7 @@ public class FreenetLinkParser implements Parser<FreenetLinkParserContext> {
         * @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);
+               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);
        }
 
        /**
@@ -261,7 +261,7 @@ public class FreenetLinkParser implements Parser<FreenetLinkParserContext> {
         * @return The part that displays the link
         */
        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);
+               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);
        }
 
 }
index 1663e08..1ac1fdd 100644 (file)
@@ -21,13 +21,19 @@ import java.io.IOException;
 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 {
+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;
@@ -35,10 +41,13 @@ public class TemplatePart implements Part {
        /**
         * Creates a new template part.
         *
+        * @param templateContextFactory
+        *            The template context factory
         * @param template
         *            The template to render
         */
-       public TemplatePart(Template template) {
+       public TemplatePart(TemplateContextFactory templateContextFactory, Template template) {
+               this.templateContextFactory = templateContextFactory;
                this.template = template;
        }
 
@@ -56,7 +65,7 @@ public class TemplatePart implements Part {
         * @return This template part (for method chaining)
         */
        public TemplatePart set(String key, Object value) {
-               template.set(key, value);
+               template.getInitialContext().set(key, value);
                return this;
        }
 
@@ -69,7 +78,15 @@ public class TemplatePart implements Part {
         */
        @Override
        public void render(Writer writer) throws IOException {
-               template.render(writer);
+               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);
        }
 
 }
index 4a0dd22..4c14457 100644 (file)
@@ -17,8 +17,8 @@
 
 package net.pterodactylus.sone.web;
 
-import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
 import net.pterodactylus.util.version.Version;
 
 /**
@@ -54,9 +54,9 @@ public class AboutPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
-               super.processTemplate(request, dataProvider);
-               dataProvider.set("version", version);
+       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+               super.processTemplate(request, templateContext);
+               templateContext.set("version", version);
        }
 
 }
diff --git a/src/main/java/net/pterodactylus/sone/web/BookmarkPage.java b/src/main/java/net/pterodactylus/sone/web/BookmarkPage.java
new file mode 100644 (file)
index 0000000..12a0934
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ * Sone - BookmarkPage.java - Copyright © 2011 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web;
+
+import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+
+/**
+ * Page that lets the user bookmark a post.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class BookmarkPage extends SoneTemplatePage {
+
+       /**
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public BookmarkPage(Template template, WebInterface webInterface) {
+               super("bookmark.html", template, "Page.Bookmark.Title", webInterface);
+       }
+
+       //
+       // SONETEMPLATEPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+               super.processTemplate(request, templateContext);
+               if (request.getMethod() == Method.POST) {
+                       String id = request.getHttpRequest().getPartAsStringFailsafe("post", 36);
+                       String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
+                       webInterface.getCore().bookmarkPost(id);
+                       throw new RedirectException(returnPage);
+               }
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/web/BookmarksPage.java b/src/main/java/net/pterodactylus/sone/web/BookmarksPage.java
new file mode 100644 (file)
index 0000000..d6f63aa
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+ * Sone - BookmarksPage.java - Copyright © 2011 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.util.collection.Pagination;
+import net.pterodactylus.util.filter.Filter;
+import net.pterodactylus.util.filter.Filters;
+import net.pterodactylus.util.number.Numbers;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+
+/**
+ * Page that lets the user browse all his bookmarked posts.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class BookmarksPage extends SoneTemplatePage {
+
+       /**
+        * Creates a new bookmarks page.
+        *
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public BookmarksPage(Template template, WebInterface webInterface) {
+               super("bookmarks.html", template, "Page.Bookmarks.Title", webInterface);
+       }
+
+       //
+       // SONETEMPLATEPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+               super.processTemplate(request, templateContext);
+               Set<Post> allPosts = webInterface.getCore().getBookmarkedPosts();
+               Set<Post> loadedPosts = Filters.filteredSet(allPosts, new Filter<Post>() {
+
+                       @Override
+                       public boolean filterObject(Post post) {
+                               return post.getSone() != null;
+                       }
+               });
+               List<Post> sortedPosts = new ArrayList<Post>(loadedPosts);
+               Collections.sort(sortedPosts, Post.TIME_COMPARATOR);
+               Pagination<Post> pagination = new Pagination<Post>(sortedPosts, 25).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("page"), 0));
+               templateContext.set("pagination", pagination);
+               templateContext.set("posts", pagination.getItems());
+               templateContext.set("postsNotLoaded", allPosts.size() != loadedPosts.size());
+       }
+
+}
index 7e1062d..7b51fb5 100644 (file)
@@ -20,8 +20,8 @@ package net.pterodactylus.sone.web;
 import net.pterodactylus.sone.data.Album;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.web.page.Page.Request.Method;
-import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
 
 /**
  * Page that lets the user create a new album.
@@ -50,12 +50,12 @@ public class CreateAlbumPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
-               super.processTemplate(request, dataProvider);
+       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+               super.processTemplate(request, templateContext);
                if (request.getMethod() == Method.POST) {
                        String name = request.getHttpRequest().getPartAsStringFailsafe("name", 64).trim();
                        if (name.length() == 0) {
-                               dataProvider.set("nameMissing", true);
+                               templateContext.set("nameMissing", true);
                                return;
                        }
                        Sone currentSone = getCurrentSone(request.getToadletContext());
index aba4700..57eb18a 100644 (file)
@@ -20,8 +20,8 @@ package net.pterodactylus.sone.web;
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.web.page.Page.Request.Method;
-import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
 
 /**
  * This page lets the user create a new {@link Post}.
@@ -50,21 +50,26 @@ public class CreatePostPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
-               super.processTemplate(request, dataProvider);
+       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+               super.processTemplate(request, templateContext);
                String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
                if (request.getMethod() == Method.POST) {
                        String text = request.getHttpRequest().getPartAsStringFailsafe("text", 65536).trim();
                        if (text.length() != 0) {
+                               String senderId = request.getHttpRequest().getPartAsStringFailsafe("sender", 43);
                                String recipientId = request.getHttpRequest().getPartAsStringFailsafe("recipient", 43);
-                               Sone recipient = webInterface.getCore().getSone(recipientId, false);
                                Sone currentSone = getCurrentSone(request.getToadletContext());
-                               webInterface.getCore().createPost(currentSone, recipient, System.currentTimeMillis(), text);
+                               Sone sender = webInterface.getCore().getLocalSone(senderId, false);
+                               if (sender == null) {
+                                       sender = currentSone;
+                               }
+                               Sone recipient = webInterface.getCore().getSone(recipientId, false);
+                               webInterface.getCore().createPost(sender, recipient, System.currentTimeMillis(), text);
                                throw new RedirectException(returnPage);
                        }
-                       dataProvider.set("errorTextEmpty", true);
+                       templateContext.set("errorTextEmpty", true);
                }
-               dataProvider.set("returnPage", returnPage);
+               templateContext.set("returnPage", returnPage);
        }
 
 }
index d4a9775..aae5e83 100644 (file)
@@ -20,8 +20,8 @@ package net.pterodactylus.sone.web;
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.web.page.Page.Request.Method;
-import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
 
 /**
  * This page lets the user post a reply to a post.
@@ -50,23 +50,27 @@ public class CreateReplyPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
-               super.processTemplate(request, dataProvider);
+       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+               super.processTemplate(request, templateContext);
                String postId = request.getHttpRequest().getPartAsStringFailsafe("post", 36);
                String text = request.getHttpRequest().getPartAsStringFailsafe("text", 65536).trim();
                String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
                if (request.getMethod() == Method.POST) {
                        Post post = webInterface.getCore().getPost(postId);
                        if (text.length() > 0) {
-                               Sone currentSone = getCurrentSone(request.getToadletContext());
-                               webInterface.getCore().createReply(currentSone, post, text);
+                               String senderId = request.getHttpRequest().getPartAsStringFailsafe("sender", 43);
+                               Sone sender = webInterface.getCore().getLocalSone(senderId, false);
+                               if (sender == null) {
+                                       sender = getCurrentSone(request.getToadletContext());
+                               }
+                               webInterface.getCore().createReply(sender, post, text);
                                throw new RedirectException(returnPage);
                        }
-                       dataProvider.set("errorTextEmpty", true);
+                       templateContext.set("errorTextEmpty", true);
                }
-               dataProvider.set("postId", postId);
-               dataProvider.set("text", text);
-               dataProvider.set("returnPage", returnPage);
+               templateContext.set("postId", postId);
+               templateContext.set("text", text);
+               templateContext.set("returnPage", returnPage);
        }
 
 }
index 04b5d37..cc40320 100644 (file)
@@ -30,8 +30,8 @@ import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.freenet.wot.OwnIdentity;
 import net.pterodactylus.sone.web.page.Page.Request.Method;
 import net.pterodactylus.util.logging.Logging;
-import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
 import freenet.clients.http.ToadletContext;
 
 /**
@@ -94,10 +94,10 @@ public class CreateSonePage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
-               super.processTemplate(request, dataProvider);
+       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+               super.processTemplate(request, templateContext);
                List<OwnIdentity> ownIdentitiesWithoutSone = getOwnIdentitiesWithoutSone(webInterface.getCore());
-               dataProvider.set("identitiesWithoutSone", ownIdentitiesWithoutSone);
+               templateContext.set("identitiesWithoutSone", ownIdentitiesWithoutSone);
                if (request.getMethod() == Method.POST) {
                        String id = request.getHttpRequest().getPartAsStringFailsafe("identity", 44);
                        OwnIdentity selectedIdentity = null;
@@ -108,11 +108,10 @@ public class CreateSonePage extends SoneTemplatePage {
                                }
                        }
                        if (selectedIdentity == null) {
-                               dataProvider.set("errorNoIdentity", true);
+                               templateContext.set("errorNoIdentity", true);
                                return;
                        }
                        /* create Sone. */
-                       webInterface.getCore().getIdentityManager().addContext(selectedIdentity, "Sone");
                        Sone sone = webInterface.getCore().createSone(selectedIdentity);
                        if (sone == null) {
                                logger.log(Level.SEVERE, "Could not create Sone for OwnIdentity: %s", selectedIdentity);
@@ -130,7 +129,7 @@ public class CreateSonePage extends SoneTemplatePage {
         */
        @Override
        public boolean isEnabled(ToadletContext toadletContext) {
-               return getCurrentSone(toadletContext) == null;
+               return (getCurrentSone(toadletContext, false) == null) || (webInterface.getCore().getLocalSones().size() == 1);
        }
 
 }
index 5722ee3..7a36d02 100644 (file)
 package net.pterodactylus.sone.web;
 
 import net.pterodactylus.sone.data.Post;
-import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.web.page.Page.Request.Method;
-import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
 
 /**
  * Lets the user delete a post they made.
@@ -50,31 +49,30 @@ public class DeletePostPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
-               super.processTemplate(request, dataProvider);
+       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+               super.processTemplate(request, templateContext);
                if (request.getMethod() == Method.GET) {
                        String postId = request.getHttpRequest().getParam("post");
                        String returnPage = request.getHttpRequest().getParam("returnPage");
                        Post post = webInterface.getCore().getPost(postId);
-                       dataProvider.set("post", post);
-                       dataProvider.set("returnPage", returnPage);
+                       templateContext.set("post", post);
+                       templateContext.set("returnPage", returnPage);
                        return;
                } else if (request.getMethod() == Method.POST) {
                        String postId = request.getHttpRequest().getPartAsStringFailsafe("post", 36);
                        String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
                        Post post = webInterface.getCore().getPost(postId);
-                       Sone currentSone = getCurrentSone(request.getToadletContext());
-                       if (!post.getSone().equals(currentSone)) {
+                       if (!webInterface.getCore().isLocalSone(post.getSone())) {
                                throw new RedirectException("noPermission.html");
                        }
                        if (request.getHttpRequest().isPartSet("confirmDelete")) {
-                               currentSone.removePost(post);
+                               webInterface.getCore().deletePost(post);
                                throw new RedirectException(returnPage);
                        } else if (request.getHttpRequest().isPartSet("abortDelete")) {
                                throw new RedirectException(returnPage);
                        }
-                       dataProvider.set("post", post);
-                       dataProvider.set("returnPage", returnPage);
+                       templateContext.set("post", post);
+                       templateContext.set("returnPage", returnPage);
                }
        }
 
diff --git a/src/main/java/net/pterodactylus/sone/web/DeleteProfileFieldPage.java b/src/main/java/net/pterodactylus/sone/web/DeleteProfileFieldPage.java
new file mode 100644 (file)
index 0000000..82f2ace
--- /dev/null
@@ -0,0 +1,84 @@
+/*
+ * Sone - DeleteProfileFieldPage.java - Copyright © 2011 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web;
+
+import net.pterodactylus.sone.data.Profile;
+import net.pterodactylus.sone.data.Profile.Field;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+
+/**
+ * Page that lets the user confirm the deletion of a profile field.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class DeleteProfileFieldPage extends SoneTemplatePage {
+
+       /**
+        * Creates a new “delete profile field” page.
+        *
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public DeleteProfileFieldPage(Template template, WebInterface webInterface) {
+               super("deleteProfileField.html", template, "Page.DeleteProfileField.Title", webInterface, true);
+       }
+
+       //
+       // SONETEMPLATEPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+               super.processTemplate(request, templateContext);
+               Sone currentSone = getCurrentSone(request.getToadletContext());
+               Profile profile = currentSone.getProfile();
+
+               /* get parameters from request. */
+               String fieldId = request.getHttpRequest().getParam("field");
+               Field field = profile.getFieldById(fieldId);
+               if (field == null) {
+                       throw new RedirectException("invalid.html");
+               }
+
+               /* process POST request. */
+               if (request.getMethod() == Method.POST) {
+                       if (request.getHttpRequest().getPartAsStringFailsafe("confirm", 4).equals("true")) {
+                               fieldId = request.getHttpRequest().getParam("field");
+                               field = profile.getFieldById(fieldId);
+                               if (field == null) {
+                                       throw new RedirectException("invalid.html");
+                               }
+                               profile.removeField(field);
+                               currentSone.setProfile(profile);
+                       }
+                       throw new RedirectException("editProfile.html#profile-fields");
+               }
+
+               /* set current values in template. */
+               templateContext.set("field", field);
+       }
+
+}
index 8d90474..782589f 100644 (file)
 package net.pterodactylus.sone.web;
 
 import net.pterodactylus.sone.data.Reply;
-import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.web.page.Page.Request.Method;
-import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
 
 /**
  * This page lets the user delete a reply.
@@ -50,14 +49,13 @@ public class DeleteReplyPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
-               super.processTemplate(request, dataProvider);
+       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+               super.processTemplate(request, templateContext);
                String replyId = request.getHttpRequest().getPartAsStringFailsafe("reply", 36);
                Reply reply = webInterface.getCore().getReply(replyId);
                String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
                if (request.getMethod() == Method.POST) {
-                       Sone currentSone = getCurrentSone(request.getToadletContext());
-                       if (!reply.getSone().equals(currentSone)) {
+                       if (!webInterface.getCore().isLocalSone(reply.getSone())) {
                                throw new RedirectException("noPermission.html");
                        }
                        if (request.getHttpRequest().isPartSet("confirmDelete")) {
@@ -67,8 +65,8 @@ public class DeleteReplyPage extends SoneTemplatePage {
                                throw new RedirectException(returnPage);
                        }
                }
-               dataProvider.set("reply", reply);
-               dataProvider.set("returnPage", returnPage);
+               templateContext.set("reply", reply);
+               templateContext.set("returnPage", returnPage);
        }
 
 }
index abe26ea..54d77b8 100644 (file)
@@ -19,8 +19,8 @@ package net.pterodactylus.sone.web;
 
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.web.page.Page.Request.Method;
-import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
 
 /**
  * Lets the user delete a Sone. Of course the Sone is not really deleted from
@@ -51,8 +51,8 @@ public class DeleteSonePage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
-               super.processTemplate(request, dataProvider);
+       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+               super.processTemplate(request, templateContext);
                if (request.getMethod() == Method.POST) {
                        if (request.getHttpRequest().isPartSet("deleteSone")) {
                                Sone currentSone = getCurrentSone(request.getToadletContext());
index 2d61a95..15c5675 100644 (file)
@@ -18,8 +18,8 @@
 package net.pterodactylus.sone.web;
 
 import net.pterodactylus.util.notify.Notification;
-import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
 
 /**
  * Page that lets the user dismiss a notification.
@@ -48,8 +48,8 @@ public class DismissNotificationPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
-               super.processTemplate(request, dataProvider);
+       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+               super.processTemplate(request, templateContext);
                String notificationId = request.getHttpRequest().getPartAsStringFailsafe("notification", 36);
                Notification notification = webInterface.getNotifications().getNotification(notificationId);
                if ((notification != null) && notification.isDismissable()) {
diff --git a/src/main/java/net/pterodactylus/sone/web/DistrustPage.java b/src/main/java/net/pterodactylus/sone/web/DistrustPage.java
new file mode 100644 (file)
index 0000000..c055545
--- /dev/null
@@ -0,0 +1,69 @@
+/*
+ * Sone - TrustPage.java - Copyright © 2011 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web;
+
+import net.pterodactylus.sone.core.Core;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+
+/**
+ * Page that lets the user distrust another Sone. This will assign a
+ * configurable (negative) amount of trust to an identity.
+ *
+ * @see Core#distrustSone(Sone, Sone)
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class DistrustPage extends SoneTemplatePage {
+
+       /**
+        * Creates a new “distrust Sone” page.
+        *
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public DistrustPage(Template template, WebInterface webInterface) {
+               super("distrust.html", template, "Page.Distrust.Title", webInterface, true);
+       }
+
+       //
+       // SONETEMPLATEPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+               super.processTemplate(request, templateContext);
+               if (request.getMethod() == Method.POST) {
+                       String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
+                       String identity = request.getHttpRequest().getPartAsStringFailsafe("sone", 44);
+                       Sone currentSone = getCurrentSone(request.getToadletContext());
+                       Sone sone = webInterface.getCore().getSone(identity, false);
+                       if (sone != null) {
+                               webInterface.getCore().distrustSone(currentSone, sone);
+                       }
+                       throw new RedirectException(returnPage);
+               }
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/web/EditProfileFieldPage.java b/src/main/java/net/pterodactylus/sone/web/EditProfileFieldPage.java
new file mode 100644 (file)
index 0000000..219bdc5
--- /dev/null
@@ -0,0 +1,90 @@
+/*
+ * Sone - EditProfileFieldPage.java - Copyright © 2011 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web;
+
+import net.pterodactylus.sone.data.Profile;
+import net.pterodactylus.sone.data.Profile.Field;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+
+/**
+ * Page that lets the user edit the name of a profile field.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class EditProfileFieldPage extends SoneTemplatePage {
+
+       /**
+        * Creates a new “edit profile field” page.
+        *
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public EditProfileFieldPage(Template template, WebInterface webInterface) {
+               super("editProfileField.html", template, "Page.EditProfileField.Title", webInterface, true);
+       }
+
+       //
+       // SONETEMPLATEPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+               super.processTemplate(request, templateContext);
+               Sone currentSone = getCurrentSone(request.getToadletContext());
+               Profile profile = currentSone.getProfile();
+
+               /* get parameters from request. */
+               String fieldId = request.getHttpRequest().getParam("field");
+               Field field = profile.getFieldById(fieldId);
+               if (field == null) {
+                       throw new RedirectException("invalid.html");
+               }
+
+               /* process the POST request. */
+               if (request.getMethod() == Method.POST) {
+                       if (request.getHttpRequest().getPartAsStringFailsafe("cancel", 4).equals("true")) {
+                               throw new RedirectException("editProfile.html#profile-fields");
+                       }
+                       fieldId = request.getHttpRequest().getPartAsStringFailsafe("field", 36);
+                       field = profile.getFieldById(fieldId);
+                       if (field == null) {
+                               throw new RedirectException("invalid.html");
+                       }
+                       String name = request.getHttpRequest().getPartAsStringFailsafe("name", 256);
+                       Field existingField = profile.getFieldByName(name);
+                       if ((existingField == null) || (existingField.equals(field))) {
+                               field.setName(name);
+                               currentSone.setProfile(profile);
+                               throw new RedirectException("editProfile.html#profile-fields");
+                       }
+                       templateContext.set("duplicateFieldName", true);
+               }
+
+               /* store current values in template. */
+               templateContext.set("field", field);
+       }
+
+}
index 01d4815..52468a3 100644 (file)
 
 package net.pterodactylus.sone.web;
 
+import java.util.List;
+
 import net.pterodactylus.sone.data.Profile;
+import net.pterodactylus.sone.data.Profile.Field;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.web.page.Page.Request.Method;
 import net.pterodactylus.util.number.Numbers;
-import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
 import freenet.clients.http.ToadletContext;
 
 /**
@@ -52,8 +55,8 @@ public class EditProfilePage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
-               super.processTemplate(request, dataProvider);
+       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+               super.processTemplate(request, templateContext);
                ToadletContext toadletContenxt = request.getToadletContext();
                Sone currentSone = getCurrentSone(toadletContenxt);
                Profile profile = currentSone.getProfile();
@@ -63,28 +66,99 @@ public class EditProfilePage extends SoneTemplatePage {
                Integer birthDay = profile.getBirthDay();
                Integer birthMonth = profile.getBirthMonth();
                Integer birthYear = profile.getBirthYear();
+               List<Field> fields = profile.getFields();
                if (request.getMethod() == Method.POST) {
-                       firstName = request.getHttpRequest().getPartAsStringFailsafe("first-name", 256).trim();
-                       middleName = request.getHttpRequest().getPartAsStringFailsafe("middle-name", 256).trim();
-                       lastName = request.getHttpRequest().getPartAsStringFailsafe("last-name", 256).trim();
-                       birthDay = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("birth-day", 256).trim());
-                       birthMonth = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("birth-month", 256).trim());
-                       birthYear = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("birth-year", 256).trim());
-                       profile.setFirstName(firstName.length() > 0 ? firstName : null);
-                       profile.setMiddleName(middleName.length() > 0 ? middleName : null);
-                       profile.setLastName(lastName.length() > 0 ? lastName : null);
-                       profile.setBirthDay(birthDay).setBirthMonth(birthMonth).setBirthYear(birthYear);
-                       if (profile.isModified()) {
+                       if (request.getHttpRequest().getPartAsStringFailsafe("save-profile", 4).equals("true")) {
+                               firstName = request.getHttpRequest().getPartAsStringFailsafe("first-name", 256).trim();
+                               middleName = request.getHttpRequest().getPartAsStringFailsafe("middle-name", 256).trim();
+                               lastName = request.getHttpRequest().getPartAsStringFailsafe("last-name", 256).trim();
+                               birthDay = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("birth-day", 256).trim());
+                               birthMonth = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("birth-month", 256).trim());
+                               birthYear = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("birth-year", 256).trim());
+                               profile.setFirstName(firstName.length() > 0 ? firstName : null);
+                               profile.setMiddleName(middleName.length() > 0 ? middleName : null);
+                               profile.setLastName(lastName.length() > 0 ? lastName : null);
+                               profile.setBirthDay(birthDay).setBirthMonth(birthMonth).setBirthYear(birthYear);
+                               for (Field field : fields) {
+                                       String value = request.getHttpRequest().getPartAsStringFailsafe("field-" + field.getId(), 400);
+                                       field.setValue(value);
+                               }
                                currentSone.setProfile(profile);
+                               webInterface.getCore().saveSone(currentSone);
+                               throw new RedirectException("editProfile.html");
+                       } else if (request.getHttpRequest().getPartAsStringFailsafe("add-field", 4).equals("true")) {
+                               String fieldName = request.getHttpRequest().getPartAsStringFailsafe("field-name", 256).trim();
+                               try {
+                                       profile.addField(fieldName);
+                                       currentSone.setProfile(profile);
+                                       fields = profile.getFields();
+                                       webInterface.getCore().saveSone(currentSone);
+                                       throw new RedirectException("editProfile.html#profile-fields");
+                               } catch (IllegalArgumentException iae1) {
+                                       templateContext.set("fieldName", fieldName);
+                                       templateContext.set("duplicateFieldName", true);
+                               }
+                       } else {
+                               String id = getFieldId(request, "delete-field-");
+                               if (id != null) {
+                                       throw new RedirectException("deleteProfileField.html?field=" + id);
+                               }
+                               id = getFieldId(request, "move-up-field-");
+                               if (id != null) {
+                                       Field field = profile.getFieldById(id);
+                                       if (field == null) {
+                                               throw new RedirectException("invalid.html");
+                                       }
+                                       profile.moveFieldUp(field);
+                                       currentSone.setProfile(profile);
+                                       throw new RedirectException("editProfile.html#profile-fields");
+                               }
+                               id = getFieldId(request, "move-down-field-");
+                               if (id != null) {
+                                       Field field = profile.getFieldById(id);
+                                       if (field == null) {
+                                               throw new RedirectException("invalid.html");
+                                       }
+                                       profile.moveFieldDown(field);
+                                       currentSone.setProfile(profile);
+                                       throw new RedirectException("editProfile.html#profile-fields");
+                               }
+                               id = getFieldId(request, "edit-field-");
+                               if (id != null) {
+                                       throw new RedirectException("editProfileField.html?field=" + id);
+                               }
                        }
-                       throw new RedirectException("index.html");
                }
-               dataProvider.set("firstName", firstName);
-               dataProvider.set("middleName", middleName);
-               dataProvider.set("lastName", lastName);
-               dataProvider.set("birthDay", birthDay);
-               dataProvider.set("birthMonth", birthMonth);
-               dataProvider.set("birthYear", birthYear);
+               templateContext.set("firstName", firstName);
+               templateContext.set("middleName", middleName);
+               templateContext.set("lastName", lastName);
+               templateContext.set("birthDay", birthDay);
+               templateContext.set("birthMonth", birthMonth);
+               templateContext.set("birthYear", birthYear);
+               templateContext.set("fields", fields);
        }
 
+       //
+       // PRIVATE METHODS
+       //
+
+       /**
+        * Searches for a part whose names starts with the given {@code String} and
+        * extracts the ID from the located name.
+        *
+        * @param request
+        *            The request to get the parts from
+        * @param partNameStart
+        *            The start of the name of the requested part
+        * @return The parsed ID, or {@code null} if there was no part matching the
+        *         given string
+        */
+       private String getFieldId(Request request, String partNameStart) {
+               for (String partName : request.getHttpRequest().getParts()) {
+                       if (partName.startsWith(partNameStart)) {
+                               return partName.substring(partNameStart.length());
+                       }
+               }
+               return null;
+       }
 }
index 864480a..9cd1f2a 100644 (file)
@@ -19,8 +19,8 @@ package net.pterodactylus.sone.web;
 
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.web.page.Page.Request.Method;
-import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
 
 /**
  * This page lets the user follow another Sone.
@@ -47,8 +47,8 @@ public class FollowSonePage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
-               super.processTemplate(request, dataProvider);
+       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+               super.processTemplate(request, templateContext);
                if (request.getMethod() == Method.POST) {
                        String soneId = request.getHttpRequest().getPartAsStringFailsafe("sone", 44);
                        String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
index c204128..6da7cda 100644 (file)
@@ -19,8 +19,8 @@ package net.pterodactylus.sone.web;
 
 import net.pterodactylus.sone.data.Album;
 import net.pterodactylus.sone.data.Image;
-import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
 
 /**
  * The image browser page is the entry page for the image management.
@@ -49,20 +49,20 @@ public class ImageBrowserPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
-               super.processTemplate(request, dataProvider);
+       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+               super.processTemplate(request, templateContext);
                String albumId = request.getHttpRequest().getParam("album", null);
                if (albumId != null) {
                        Album album = webInterface.getCore().getAlbum(albumId, false);
-                       dataProvider.set("albumRequested", true);
-                       dataProvider.set("album", album);
+                       templateContext.set("albumRequested", true);
+                       templateContext.set("album", album);
                        return;
                }
                String imageId = request.getHttpRequest().getParam("image", null);
                if (imageId != null) {
                        Image image = webInterface.getCore().getImage(imageId, false);
-                       dataProvider.set("imageRequested", true);
-                       dataProvider.set("image", image);
+                       templateContext.set("imageRequested", true);
+                       templateContext.set("image", image);
                }
        }
 }
index 825267b..2d85e02 100644 (file)
@@ -24,8 +24,10 @@ import java.util.List;
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Reply;
 import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.util.template.DataProvider;
+import net.pterodactylus.util.collection.Pagination;
+import net.pterodactylus.util.number.Numbers;
 import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
 
 /**
  * The index page shows the main page of Sone. This page will contain the posts
@@ -53,8 +55,8 @@ public class IndexPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
-               super.processTemplate(request, dataProvider);
+       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+               super.processTemplate(request, templateContext);
                Sone currentSone = getCurrentSone(request.getToadletContext());
                List<Post> allPosts = new ArrayList<Post>();
                allPosts.addAll(currentSone.getPosts());
@@ -72,16 +74,18 @@ public class IndexPage extends SoneTemplatePage {
                        }
                }
                Collections.sort(allPosts, Post.TIME_COMPARATOR);
-               dataProvider.set("posts", allPosts);
+               Pagination<Post> pagination = new Pagination<Post>(allPosts, 25).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("page"), 0));
+               templateContext.set("pagination", pagination);
+               templateContext.set("posts", pagination.getItems());
        }
 
        /**
         * {@inheritDoc}
         */
        @Override
-       protected void postProcess(Request request, DataProvider dataProvider) {
+       protected void postProcess(Request request, TemplateContext templateContext) {
                @SuppressWarnings("unchecked")
-               List<Post> posts = (List<Post>) dataProvider.get("posts");
+               List<Post> posts = (List<Post>) templateContext.get("posts");
                for (Post post : posts) {
                        webInterface.getCore().markPostKnown(post);
                        for (Reply reply : webInterface.getCore().getReplies(post)) {
index 774f410..b08b6e3 100644 (file)
@@ -22,8 +22,10 @@ import java.util.Collections;
 import java.util.List;
 
 import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.util.template.DataProvider;
+import net.pterodactylus.util.collection.Pagination;
+import net.pterodactylus.util.number.Numbers;
 import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
 
 /**
  * This page shows all known Sones.
@@ -52,11 +54,26 @@ public class KnownSonesPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
-               super.processTemplate(request, dataProvider);
+       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+               super.processTemplate(request, templateContext);
                List<Sone> knownSones = new ArrayList<Sone>(webInterface.getCore().getSones());
                Collections.sort(knownSones, Sone.NICE_NAME_COMPARATOR);
-               dataProvider.set("knownSones", knownSones);
+               Pagination<Sone> sonePagination = new Pagination<Sone>(knownSones, 25).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("page"), 0));
+               templateContext.set("pagination", sonePagination);
+               templateContext.set("knownSones", sonePagination.getItems());
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void postProcess(Request request, TemplateContext templateContext) {
+               super.postProcess(request, templateContext);
+               @SuppressWarnings("unchecked")
+               List<Sone> sones = (List<Sone>) templateContext.get("knownSones");
+               for (Sone sone : sones) {
+                       webInterface.getCore().markSoneKnown(sone);
+               }
        }
 
 }
index c84ea57..73fdd3c 100644 (file)
@@ -20,8 +20,8 @@ package net.pterodactylus.sone.web;
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.web.page.Page.Request.Method;
-import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
 
 /**
  * Page that lets the user like a {@link Post}.
@@ -50,8 +50,8 @@ public class LikePage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
-               super.processTemplate(request, dataProvider);
+       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+               super.processTemplate(request, templateContext);
                if (request.getMethod() == Method.POST) {
                        String type=request.getHttpRequest().getPartAsStringFailsafe("type", 16);
                        String id = request.getHttpRequest().getPartAsStringFailsafe(type, 36);
index 7888659..d09de66 100644 (file)
@@ -18,8 +18,8 @@
 package net.pterodactylus.sone.web;
 
 import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
 
 /**
  * This page lets the user lock a {@link Sone} to prevent it from being
@@ -49,8 +49,8 @@ public class LockSonePage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
-               super.processTemplate(request, dataProvider);
+       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+               super.processTemplate(request, templateContext);
                String soneId = request.getHttpRequest().getPartAsStringFailsafe("sone", 44);
                Sone sone = webInterface.getCore().getLocalSone(soneId, false);
                if (sone != null) {
index ff81562..eaa8351 100644 (file)
@@ -26,8 +26,8 @@ import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.freenet.wot.OwnIdentity;
 import net.pterodactylus.sone.web.page.Page.Request.Method;
 import net.pterodactylus.util.logging.Logging;
-import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
 import freenet.clients.http.ToadletContext;
 
 /**
@@ -61,22 +61,26 @@ public class LoginPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
-               super.processTemplate(request, dataProvider);
+       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+               super.processTemplate(request, templateContext);
                /* get all own identities. */
                List<Sone> localSones = new ArrayList<Sone>(webInterface.getCore().getLocalSones());
                Collections.sort(localSones, Sone.NICE_NAME_COMPARATOR);
-               dataProvider.set("sones", localSones);
+               templateContext.set("sones", localSones);
                if (request.getMethod() == Method.POST) {
                        String soneId = request.getHttpRequest().getPartAsStringFailsafe("sone-id", 100);
                        Sone selectedSone = webInterface.getCore().getLocalSone(soneId, false);
                        if (selectedSone != null) {
                                setCurrentSone(request.getToadletContext(), selectedSone);
-                               throw new RedirectException("index.html");
+                               String target = request.getHttpRequest().getParam("target");
+                               if ((target == null) || (target.length() == 0)) {
+                                       target = "index.html";
+                               }
+                               throw new RedirectException(target);
                        }
                }
                List<OwnIdentity> ownIdentitiesWithoutSone = CreateSonePage.getOwnIdentitiesWithoutSone(webInterface.getCore());
-               dataProvider.set("identitiesWithoutSone", ownIdentitiesWithoutSone);
+               templateContext.set("identitiesWithoutSone", ownIdentitiesWithoutSone);
        }
 
        /**
@@ -84,7 +88,7 @@ public class LoginPage extends SoneTemplatePage {
         */
        @Override
        protected String getRedirectTarget(Request request) {
-               if (getCurrentSone(request.getToadletContext()) != null) {
+               if (getCurrentSone(request.getToadletContext(), false) != null) {
                        return "index.html";
                }
                return null;
@@ -99,7 +103,7 @@ public class LoginPage extends SoneTemplatePage {
         */
        @Override
        public boolean isEnabled(ToadletContext toadletContext) {
-               return getCurrentSone(toadletContext) == null;
+               return getCurrentSone(toadletContext, false) == null;
        }
 
 }
index 0b6e6e7..f388368 100644 (file)
@@ -17,8 +17,8 @@
 
 package net.pterodactylus.sone.web;
 
-import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
 import freenet.clients.http.ToadletContext;
 
 /**
@@ -46,9 +46,9 @@ public class LogoutPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
+       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
                setCurrentSone(request.getToadletContext(), null);
-               super.processTemplate(request, dataProvider);
+               super.processTemplate(request, templateContext);
                throw new RedirectException("index.html");
        }
 
@@ -57,7 +57,7 @@ public class LogoutPage extends SoneTemplatePage {
         */
        @Override
        public boolean isEnabled(ToadletContext toadletContext) {
-               return getCurrentSone(toadletContext) != null;
+               return (getCurrentSone(toadletContext, false) != null) && (webInterface.getCore().getLocalSones().size() != 1);
        }
 
 }
diff --git a/src/main/java/net/pterodactylus/sone/web/MarkAsKnownPage.java b/src/main/java/net/pterodactylus/sone/web/MarkAsKnownPage.java
new file mode 100644 (file)
index 0000000..4ecf6e0
--- /dev/null
@@ -0,0 +1,89 @@
+/*
+ * Sone - MarkReadPage.java - Copyright © 2011 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web;
+
+import java.util.StringTokenizer;
+
+import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.sone.data.Reply;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+
+/**
+ * Page that lets the user mark a number of {@link Sone}s, {@link Post}s, or
+ * {@link Reply Replie}s as known.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class MarkAsKnownPage extends SoneTemplatePage {
+
+       /**
+        * Creates a new “mark as known” page.
+        *
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public MarkAsKnownPage(Template template, WebInterface webInterface) {
+               super("markAsKnown.html", template, "Page.MarkAsKnown.Title", webInterface);
+       }
+
+       //
+       // SONETEMPLATEPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+               super.processTemplate(request, templateContext);
+               String type = request.getHttpRequest().getPartAsStringFailsafe("type", 5);
+               if (!type.equals("sone") && !type.equals("post") && !type.equals("reply")) {
+                       throw new RedirectException("invalid.html");
+               }
+               String ids = request.getHttpRequest().getPartAsStringFailsafe("id", 65536);
+               for (StringTokenizer idTokenizer = new StringTokenizer(ids); idTokenizer.hasMoreTokens();) {
+                       String id = idTokenizer.nextToken();
+                       if (type.equals("post")) {
+                               Post post = webInterface.getCore().getPost(id, false);
+                               if (post == null) {
+                                       continue;
+                               }
+                               webInterface.getCore().markPostKnown(post);
+                       } else if (type.equals("reply")) {
+                               Reply reply = webInterface.getCore().getReply(id, false);
+                               if (reply == null) {
+                                       continue;
+                               }
+                               webInterface.getCore().markReplyKnown(reply);
+                       } else if (type.equals("sone")) {
+                               Sone sone = webInterface.getCore().getSone(id, false);
+                               if (sone == null) {
+                                       continue;
+                               }
+                               webInterface.getCore().markSoneKnown(sone);
+                       }
+               }
+               String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
+               throw new RedirectException(returnPage);
+       }
+
+}
index 63ed6a7..84d7c79 100644 (file)
 
 package net.pterodactylus.sone.web;
 
-import net.pterodactylus.sone.core.Options;
+import net.pterodactylus.sone.core.Core.Preferences;
 import net.pterodactylus.sone.web.page.Page.Request.Method;
 import net.pterodactylus.util.number.Numbers;
-import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
 
 /**
  * This page lets the user edit the options of the Sone plugin.
@@ -50,25 +50,37 @@ public class OptionsPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
-               super.processTemplate(request, dataProvider);
-               Options options = webInterface.getCore().getOptions();
+       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+               super.processTemplate(request, templateContext);
+               Preferences preferences = webInterface.getCore().getPreferences();
                if (request.getMethod() == Method.POST) {
                        Integer insertionDelay = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("insertion-delay", 16));
-                       options.getIntegerOption("InsertionDelay").set(insertionDelay);
+                       preferences.setInsertionDelay(insertionDelay);
+                       Integer positiveTrust = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("positive-trust", 3));
+                       preferences.setPositiveTrust(positiveTrust);
+                       Integer negativeTrust = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("negative-trust", 4));
+                       preferences.setNegativeTrust(negativeTrust);
+                       String trustComment = request.getHttpRequest().getPartAsStringFailsafe("trust-comment", 256);
+                       if (trustComment.trim().length() == 0) {
+                               trustComment = null;
+                       }
+                       preferences.setTrustComment(trustComment);
                        boolean soneRescueMode = Boolean.parseBoolean(request.getHttpRequest().getPartAsStringFailsafe("sone-rescue-mode", 5));
-                       options.getBooleanOption("SoneRescueMode").set(soneRescueMode);
+                       preferences.setSoneRescueMode(soneRescueMode);
                        boolean clearOnNextRestart = Boolean.parseBoolean(request.getHttpRequest().getPartAsStringFailsafe("clear-on-next-restart", 5));
-                       options.getBooleanOption("ClearOnNextRestart").set(clearOnNextRestart);
+                       preferences.setClearOnNextRestart(clearOnNextRestart);
                        boolean reallyClearOnNextRestart = Boolean.parseBoolean(request.getHttpRequest().getPartAsStringFailsafe("really-clear-on-next-restart", 5));
-                       options.getBooleanOption("ReallyClearOnNextRestart").set(reallyClearOnNextRestart);
+                       preferences.setReallyClearOnNextRestart(reallyClearOnNextRestart);
                        webInterface.getCore().saveConfiguration();
                        throw new RedirectException(getPath());
                }
-               dataProvider.set("insertion-delay", options.getIntegerOption("InsertionDelay").get());
-               dataProvider.set("sone-rescue-mode", options.getBooleanOption("SoneRescueMode").get());
-               dataProvider.set("clear-on-next-restart", options.getBooleanOption("ClearOnNextRestart").get());
-               dataProvider.set("really-clear-on-next-restart", options.getBooleanOption("ReallyClearOnNextRestart").get());
+               templateContext.set("insertion-delay", preferences.getInsertionDelay());
+               templateContext.set("positive-trust", preferences.getPositiveTrust());
+               templateContext.set("negative-trust", preferences.getNegativeTrust());
+               templateContext.set("trust-comment", preferences.getTrustComment());
+               templateContext.set("sone-rescue-mode", preferences.isSoneRescueMode());
+               templateContext.set("clear-on-next-restart", preferences.isClearOnNextRestart());
+               templateContext.set("really-clear-on-next-restart", preferences.isReallyClearOnNextRestart());
        }
 
 }
index dc41865..a04873d 100644 (file)
@@ -17,6 +17,8 @@
 
 package net.pterodactylus.sone.web;
 
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
 import java.util.Arrays;
 import java.util.Collection;
 
@@ -24,10 +26,11 @@ 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.DataProvider;
 import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
 import freenet.clients.http.SessionManager.Session;
 import freenet.clients.http.ToadletContext;
+import freenet.support.api.HTTPRequest;
 
 /**
  * Base page for the Freetalk web interface.
@@ -74,10 +77,10 @@ public class SoneTemplatePage extends TemplatePage {
         *            Whether this page requires a login
         */
        public SoneTemplatePage(String path, Template template, String pageTitleKey, WebInterface webInterface, boolean requireLogin) {
-               super(path, template, webInterface.getL10n(), pageTitleKey, "noPermission.html");
+               super(path, webInterface.getTemplateContextFactory(), template, webInterface.getL10n(), pageTitleKey, "noPermission.html");
                this.webInterface = webInterface;
                this.requireLogin = requireLogin;
-               template.set("webInterface", webInterface);
+               template.getInitialContext().set("webInterface", webInterface);
        }
 
        //
@@ -186,14 +189,16 @@ public class SoneTemplatePage extends TemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
-               super.processTemplate(request, dataProvider);
-               dataProvider.set("currentSone", getCurrentSone(request.getToadletContext(), false));
-               dataProvider.set("request", request);
-               dataProvider.set("currentVersion", SonePlugin.VERSION);
-               dataProvider.set("hasLatestVersion", webInterface.getCore().getUpdateChecker().hasLatestVersion());
-               dataProvider.set("latestVersion", webInterface.getCore().getUpdateChecker().getLatestVersion());
-               dataProvider.set("latestVersionTime", webInterface.getCore().getUpdateChecker().getLatestVersionDate());
+       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+               super.processTemplate(request, templateContext);
+               templateContext.set("currentSone", getCurrentSone(request.getToadletContext(), false));
+               templateContext.set("localSones", webInterface.getCore().getLocalSones());
+               templateContext.set("request", request);
+               templateContext.set("currentVersion", SonePlugin.VERSION);
+               templateContext.set("hasLatestVersion", webInterface.getCore().getUpdateChecker().hasLatestVersion());
+               templateContext.set("latestEdition", webInterface.getCore().getUpdateChecker().getLatestEdition());
+               templateContext.set("latestVersion", webInterface.getCore().getUpdateChecker().getLatestVersion());
+               templateContext.set("latestVersionTime", webInterface.getCore().getUpdateChecker().getLatestVersionDate());
        }
 
        /**
@@ -202,7 +207,26 @@ public class SoneTemplatePage extends TemplatePage {
        @Override
        protected String getRedirectTarget(Page.Request request) {
                if (requiresLogin() && (getCurrentSone(request.getToadletContext(), false) == null)) {
-                       return "login.html";
+                       HTTPRequest httpRequest = request.getHttpRequest();
+                       String originalUrl = httpRequest.getPath();
+                       if (httpRequest.hasParameters()) {
+                               StringBuilder requestParameters = new StringBuilder();
+                               for (String parameterName : httpRequest.getParameterNames()) {
+                                       if (requestParameters.length() > 0) {
+                                               requestParameters.append("%26");
+                                       }
+                                       String[] parameterValues = httpRequest.getMultipleParam(parameterName);
+                                       for (String parameterValue : parameterValues) {
+                                               try {
+                                                       requestParameters.append(URLEncoder.encode(parameterName, "UTF-8")).append("%3d").append(URLEncoder.encode(parameterValue, "UTF-8"));
+                                               } catch (UnsupportedEncodingException uee1) {
+                                                       /* A JVM without UTF-8? I don’t think so. */
+                                               }
+                                       }
+                               }
+                               originalUrl += "?" + requestParameters.toString();
+                       }
+                       return "login.html?target=" + originalUrl;
                }
                return null;
        }
diff --git a/src/main/java/net/pterodactylus/sone/web/TrustPage.java b/src/main/java/net/pterodactylus/sone/web/TrustPage.java
new file mode 100644 (file)
index 0000000..b0dadef
--- /dev/null
@@ -0,0 +1,69 @@
+/*
+ * Sone - TrustPage.java - Copyright © 2011 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web;
+
+import net.pterodactylus.sone.core.Core;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+
+/**
+ * Page that lets the user trust another Sone. This will assign a configurable
+ * amount of trust to an identity.
+ *
+ * @see Core#trustSone(Sone, Sone)
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class TrustPage extends SoneTemplatePage {
+
+       /**
+        * Creates a new “trust Sone” page.
+        *
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public TrustPage(Template template, WebInterface webInterface) {
+               super("trust.html", template, "Page.Trust.Title", webInterface, true);
+       }
+
+       //
+       // SONETEMPLATEPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+               super.processTemplate(request, templateContext);
+               if (request.getMethod() == Method.POST) {
+                       String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
+                       String identity = request.getHttpRequest().getPartAsStringFailsafe("sone", 44);
+                       Sone currentSone = getCurrentSone(request.getToadletContext());
+                       Sone sone = webInterface.getCore().getSone(identity, false);
+                       if (sone != null) {
+                               webInterface.getCore().trustSone(currentSone, sone);
+                       }
+                       throw new RedirectException(returnPage);
+               }
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/web/UnbookmarkPage.java b/src/main/java/net/pterodactylus/sone/web/UnbookmarkPage.java
new file mode 100644 (file)
index 0000000..85f46f0
--- /dev/null
@@ -0,0 +1,72 @@
+/*
+ * Sone - BookmarkPage.java - Copyright © 2011 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web;
+
+import java.util.Set;
+
+import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+
+/**
+ * Page that lets the user unbookmark a post.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class UnbookmarkPage extends SoneTemplatePage {
+
+       /**
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public UnbookmarkPage(Template template, WebInterface webInterface) {
+               super("unbookmark.html", template, "Page.Unbookmark.Title", webInterface);
+       }
+
+       //
+       // SONETEMPLATEPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+               super.processTemplate(request, templateContext);
+               if (request.getMethod() == Method.POST) {
+                       String id = request.getHttpRequest().getPartAsStringFailsafe("post", 36);
+                       String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
+                       webInterface.getCore().unbookmarkPost(id);
+                       throw new RedirectException(returnPage);
+               }
+               String id = request.getHttpRequest().getParam("post");
+               if (id.equals("allNotLoaded")) {
+                       Set<Post> posts = webInterface.getCore().getBookmarkedPosts();
+                       for (Post post : posts) {
+                               if (post.getSone() == null) {
+                                       webInterface.getCore().unbookmark(post);
+                               }
+                       }
+                       throw new RedirectException("bookmarks.html");
+               }
+       }
+
+}
index 316e408..e026a9a 100644 (file)
@@ -19,8 +19,8 @@ package net.pterodactylus.sone.web;
 
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.web.page.Page.Request.Method;
-import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
 
 /**
  * This page lets the user unfollow another Sone.
@@ -47,8 +47,8 @@ public class UnfollowSonePage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
-               super.processTemplate(request, dataProvider);
+       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+               super.processTemplate(request, templateContext);
                if (request.getMethod() == Method.POST) {
                        String soneId = request.getHttpRequest().getPartAsStringFailsafe("sone", 44);
                        String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
index d08689f..24ff3ca 100644 (file)
@@ -20,8 +20,8 @@ package net.pterodactylus.sone.web;
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.web.page.Page.Request.Method;
-import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
 
 /**
  * Page that lets the user unlike a {@link Post}.
@@ -50,8 +50,8 @@ public class UnlikePage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
-               super.processTemplate(request, dataProvider);
+       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+               super.processTemplate(request, templateContext);
                if (request.getMethod() == Method.POST) {
                        String type = request.getHttpRequest().getPartAsStringFailsafe("type", 16);
                        String id = request.getHttpRequest().getPartAsStringFailsafe(type, 36);
index 35d69ff..5408f20 100644 (file)
@@ -18,8 +18,8 @@
 package net.pterodactylus.sone.web;
 
 import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
 
 /**
  * This page lets the user unlock a {@link Sone} to allow its insertion.
@@ -48,8 +48,8 @@ public class UnlockSonePage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
-               super.processTemplate(request, dataProvider);
+       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+               super.processTemplate(request, templateContext);
                String soneId = request.getHttpRequest().getPartAsStringFailsafe("sone", 44);
                Sone sone = webInterface.getCore().getLocalSone(soneId, false);
                if (sone != null) {
diff --git a/src/main/java/net/pterodactylus/sone/web/UntrustPage.java b/src/main/java/net/pterodactylus/sone/web/UntrustPage.java
new file mode 100644 (file)
index 0000000..0e7e198
--- /dev/null
@@ -0,0 +1,69 @@
+/*
+ * Sone - TrustPage.java - Copyright © 2011 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web;
+
+import net.pterodactylus.sone.core.Core;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+
+/**
+ * Page that lets the user untrust another Sone. This will remove all trust
+ * assignments for an identity.
+ *
+ * @see Core#untrustSone(Sone, Sone)
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class UntrustPage extends SoneTemplatePage {
+
+       /**
+        * Creates a new “untrust Sone” page.
+        *
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public UntrustPage(Template template, WebInterface webInterface) {
+               super("untrust.html", template, "Page.Untrust.Title", webInterface, true);
+       }
+
+       //
+       // SONETEMPLATEPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+               super.processTemplate(request, templateContext);
+               if (request.getMethod() == Method.POST) {
+                       String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
+                       String identity = request.getHttpRequest().getPartAsStringFailsafe("sone", 44);
+                       Sone currentSone = getCurrentSone(request.getToadletContext());
+                       Sone sone = webInterface.getCore().getSone(identity, false);
+                       if (sone != null) {
+                               webInterface.getCore().untrustSone(currentSone, sone);
+                       }
+                       throw new RedirectException(returnPage);
+               }
+       }
+
+}
index 3c4eddf..967b9c5 100644 (file)
@@ -19,8 +19,8 @@ package net.pterodactylus.sone.web;
 
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Reply;
-import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
 
 /**
  * This page lets the user view a post and all its replies.
@@ -49,19 +49,24 @@ public class ViewPostPage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
-               super.processTemplate(request, dataProvider);
+       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+               super.processTemplate(request, templateContext);
                String postId = request.getHttpRequest().getParam("post");
+               boolean raw = request.getHttpRequest().getParam("raw").equals("true");
                Post post = webInterface.getCore().getPost(postId);
-               dataProvider.set("post", post);
+               templateContext.set("post", post);
+               templateContext.set("raw", raw);
        }
 
        /**
         * {@inheritDoc}
         */
        @Override
-       protected void postProcess(Request request, DataProvider dataProvider) {
-               Post post = (Post) dataProvider.get("post");
+       protected void postProcess(Request request, TemplateContext templateContext) {
+               Post post = (Post) templateContext.get("post");
+               if (post == null) {
+                       return;
+               }
                webInterface.getCore().markPostKnown(post);
                for (Reply reply : webInterface.getCore().getReplies(post)) {
                        webInterface.getCore().markReplyKnown(reply);
index 9fd6fae..0792a6f 100644 (file)
@@ -22,8 +22,8 @@ import java.util.List;
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Reply;
 import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
 
 /**
  * Lets the user browser another Sone.
@@ -52,19 +52,23 @@ public class ViewSonePage extends SoneTemplatePage {
         * {@inheritDoc}
         */
        @Override
-       protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
-               super.processTemplate(request, dataProvider);
+       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+               super.processTemplate(request, templateContext);
                String soneId = request.getHttpRequest().getParam("sone");
                Sone sone = webInterface.getCore().getSone(soneId, false);
-               dataProvider.set("sone", sone);
+               templateContext.set("sone", sone);
        }
 
        /**
         * {@inheritDoc}
         */
        @Override
-       protected void postProcess(Request request, DataProvider dataProvider) {
-               Sone sone = (Sone) dataProvider.get("sone");
+       protected void postProcess(Request request, TemplateContext templateContext) {
+               Sone sone = (Sone) templateContext.get("sone");
+               if (sone == null) {
+                       return;
+               }
+               webInterface.getCore().markSoneKnown(sone);
                List<Post> posts = sone.getPosts();
                for (Post post : posts) {
                        webInterface.getCore().markPostKnown(post);
index 66ba789..e3a95d4 100644 (file)
@@ -42,6 +42,7 @@ import net.pterodactylus.sone.data.Reply;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.freenet.L10nFilter;
 import net.pterodactylus.sone.freenet.wot.Identity;
+import net.pterodactylus.sone.freenet.wot.Trust;
 import net.pterodactylus.sone.main.SonePlugin;
 import net.pterodactylus.sone.notify.ListNotification;
 import net.pterodactylus.sone.template.AlbumAccessor;
@@ -49,17 +50,25 @@ import net.pterodactylus.sone.template.CollectionAccessor;
 import net.pterodactylus.sone.template.CssClassNameFilter;
 import net.pterodactylus.sone.template.GetPagePlugin;
 import net.pterodactylus.sone.template.IdentityAccessor;
+import net.pterodactylus.sone.template.JavascriptFilter;
 import net.pterodactylus.sone.template.NotificationManagerAccessor;
+import net.pterodactylus.sone.template.ParserFilter;
 import net.pterodactylus.sone.template.PostAccessor;
 import net.pterodactylus.sone.template.ReplyAccessor;
 import net.pterodactylus.sone.template.RequestChangeFilter;
 import net.pterodactylus.sone.template.SoneAccessor;
 import net.pterodactylus.sone.template.SubstringFilter;
+import net.pterodactylus.sone.template.TrustAccessor;
+import net.pterodactylus.sone.template.UnknownDateFilter;
+import net.pterodactylus.sone.web.ajax.BookmarkAjaxPage;
 import net.pterodactylus.sone.web.ajax.CreatePostAjaxPage;
 import net.pterodactylus.sone.web.ajax.CreateReplyAjaxPage;
 import net.pterodactylus.sone.web.ajax.DeletePostAjaxPage;
+import net.pterodactylus.sone.web.ajax.DeleteProfileFieldAjaxPage;
 import net.pterodactylus.sone.web.ajax.DeleteReplyAjaxPage;
 import net.pterodactylus.sone.web.ajax.DismissNotificationAjaxPage;
+import net.pterodactylus.sone.web.ajax.DistrustAjaxPage;
+import net.pterodactylus.sone.web.ajax.EditProfileFieldAjaxPage;
 import net.pterodactylus.sone.web.ajax.FollowSoneAjaxPage;
 import net.pterodactylus.sone.web.ajax.GetLikesAjaxPage;
 import net.pterodactylus.sone.web.ajax.GetPostAjaxPage;
@@ -68,27 +77,42 @@ import net.pterodactylus.sone.web.ajax.GetStatusAjaxPage;
 import net.pterodactylus.sone.web.ajax.GetTranslationPage;
 import net.pterodactylus.sone.web.ajax.LikeAjaxPage;
 import net.pterodactylus.sone.web.ajax.LockSoneAjaxPage;
-import net.pterodactylus.sone.web.ajax.MarkPostAsKnownPage;
-import net.pterodactylus.sone.web.ajax.MarkReplyAsKnownPage;
+import net.pterodactylus.sone.web.ajax.MarkAsKnownAjaxPage;
+import net.pterodactylus.sone.web.ajax.MoveProfileFieldAjaxPage;
+import net.pterodactylus.sone.web.ajax.TrustAjaxPage;
+import net.pterodactylus.sone.web.ajax.UnbookmarkAjaxPage;
 import net.pterodactylus.sone.web.ajax.UnfollowSoneAjaxPage;
 import net.pterodactylus.sone.web.ajax.UnlikeAjaxPage;
 import net.pterodactylus.sone.web.ajax.UnlockSoneAjaxPage;
+import net.pterodactylus.sone.web.ajax.UntrustAjaxPage;
 import net.pterodactylus.sone.web.page.PageToadlet;
 import net.pterodactylus.sone.web.page.PageToadletFactory;
 import net.pterodactylus.sone.web.page.StaticPage;
+import net.pterodactylus.util.cache.Cache;
+import net.pterodactylus.util.cache.CacheException;
+import net.pterodactylus.util.cache.CacheItem;
+import net.pterodactylus.util.cache.DefaultCacheItem;
+import net.pterodactylus.util.cache.MemoryCache;
+import net.pterodactylus.util.cache.ValueRetriever;
 import net.pterodactylus.util.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.DateFilter;
-import net.pterodactylus.util.template.DefaultTemplateFactory;
+import net.pterodactylus.util.template.FormatFilter;
+import net.pterodactylus.util.template.HtmlFilter;
 import net.pterodactylus.util.template.MatchFilter;
 import net.pterodactylus.util.template.PaginationPlugin;
+import net.pterodactylus.util.template.Provider;
 import net.pterodactylus.util.template.ReflectionAccessor;
+import net.pterodactylus.util.template.ReplaceFilter;
+import net.pterodactylus.util.template.StoreFilter;
 import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.template.TemplateContextFactory;
 import net.pterodactylus.util.template.TemplateException;
-import net.pterodactylus.util.template.TemplateFactory;
-import net.pterodactylus.util.template.TemplateProvider;
+import net.pterodactylus.util.template.TemplateParser;
 import net.pterodactylus.util.template.XmlFilter;
 import net.pterodactylus.util.thread.Ticker;
 import net.pterodactylus.util.version.Version;
@@ -121,8 +145,8 @@ public class WebInterface implements CoreListener {
        /** The form password. */
        private final String formPassword;
 
-       /** The template factory. */
-       private DefaultTemplateFactory templateFactory;
+       /** The template context factory. */
+       private final TemplateContextFactory templateContextFactory;
 
        /** The “new Sone” notification. */
        private final ListNotification<Sone> newSoneNotification;
@@ -154,51 +178,62 @@ public class WebInterface implements CoreListener {
         * @param sonePlugin
         *            The Sone plugin
         */
+       @SuppressWarnings("synthetic-access")
        public WebInterface(SonePlugin sonePlugin) {
                this.sonePlugin = sonePlugin;
                formPassword = sonePlugin.pluginRespirator().getToadletContainer().getFormPassword();
 
-               templateFactory = new DefaultTemplateFactory();
-               templateFactory.addAccessor(Object.class, new ReflectionAccessor());
-               templateFactory.addAccessor(Collection.class, new CollectionAccessor());
-               templateFactory.addAccessor(Sone.class, new SoneAccessor(getCore()));
-               templateFactory.addAccessor(Post.class, new PostAccessor(getCore(), templateFactory));
-               templateFactory.addAccessor(Reply.class, new ReplyAccessor(getCore(), templateFactory));
-               templateFactory.addAccessor(Album.class, new AlbumAccessor());
-               templateFactory.addAccessor(Identity.class, new IdentityAccessor(getCore()));
-               templateFactory.addAccessor(NotificationManager.class, new NotificationManagerAccessor());
-               templateFactory.addFilter("date", new DateFilter());
-               templateFactory.addFilter("l10n", new L10nFilter(getL10n()));
-               templateFactory.addFilter("substring", new SubstringFilter());
-               templateFactory.addFilter("xml", new XmlFilter());
-               templateFactory.addFilter("change", new RequestChangeFilter());
-               templateFactory.addFilter("match", new MatchFilter());
-               templateFactory.addFilter("css", new CssClassNameFilter());
-               templateFactory.addPlugin("getpage", new GetPagePlugin());
-               templateFactory.addPlugin("paginate", new PaginationPlugin());
-               templateFactory.setTemplateProvider(new ClassPathTemplateProvider(templateFactory));
-               templateFactory.addTemplateObject("formPassword", formPassword);
+               templateContextFactory = new TemplateContextFactory();
+               templateContextFactory.addAccessor(Object.class, new ReflectionAccessor());
+               templateContextFactory.addAccessor(Collection.class, new CollectionAccessor());
+               templateContextFactory.addAccessor(Sone.class, new SoneAccessor(getCore()));
+               templateContextFactory.addAccessor(Post.class, new PostAccessor(getCore()));
+               templateContextFactory.addAccessor(Reply.class, new ReplyAccessor(getCore()));
+               templateContextFactory.addAccessor(Album.class, new AlbumAccessor());
+               templateContextFactory.addAccessor(Identity.class, new IdentityAccessor(getCore()));
+               templateContextFactory.addAccessor(NotificationManager.class, new NotificationManagerAccessor());
+               templateContextFactory.addAccessor(Trust.class, new TrustAccessor());
+               templateContextFactory.addFilter("date", new DateFilter());
+               templateContextFactory.addFilter("html", new HtmlFilter());
+               templateContextFactory.addFilter("replace", new ReplaceFilter());
+               templateContextFactory.addFilter("store", new StoreFilter());
+               templateContextFactory.addFilter("l10n", new L10nFilter(getL10n()));
+               templateContextFactory.addFilter("substring", new SubstringFilter());
+               templateContextFactory.addFilter("xml", new XmlFilter());
+               templateContextFactory.addFilter("change", new RequestChangeFilter());
+               templateContextFactory.addFilter("match", new MatchFilter());
+               templateContextFactory.addFilter("css", new CssClassNameFilter());
+               templateContextFactory.addFilter("js", new JavascriptFilter());
+               templateContextFactory.addFilter("parse", new ParserFilter(getCore(), templateContextFactory));
+               templateContextFactory.addFilter("unknown", new UnknownDateFilter(getL10n(), "View.Sone.Text.UnknownDate"));
+               templateContextFactory.addFilter("format", new FormatFilter());
+               templateContextFactory.addFilter("sort", new CollectionSortFilter());
+               templateContextFactory.addPlugin("getpage", new GetPagePlugin());
+               templateContextFactory.addPlugin("paginate", new PaginationPlugin());
+               templateContextFactory.addProvider(Provider.TEMPLATE_CONTEXT_PROVIDER);
+               templateContextFactory.addProvider(new ClassPathTemplateProvider());
+               templateContextFactory.addTemplateObject("formPassword", formPassword);
 
                /* create notifications. */
-               Template newSoneNotificationTemplate = templateFactory.createTemplate(createReader("/templates/notify/newSoneNotification.html"));
+               Template newSoneNotificationTemplate = TemplateParser.parse(createReader("/templates/notify/newSoneNotification.html"));
                newSoneNotification = new ListNotification<Sone>("new-sone-notification", "sones", newSoneNotificationTemplate);
 
-               Template newPostNotificationTemplate = templateFactory.createTemplate(createReader("/templates/notify/newPostNotification.html"));
+               Template newPostNotificationTemplate = TemplateParser.parse(createReader("/templates/notify/newPostNotification.html"));
                newPostNotification = new ListNotification<Post>("new-post-notification", "posts", newPostNotificationTemplate);
 
-               Template newReplyNotificationTemplate = templateFactory.createTemplate(createReader("/templates/notify/newReplyNotification.html"));
+               Template newReplyNotificationTemplate = TemplateParser.parse(createReader("/templates/notify/newReplyNotification.html"));
                newReplyNotification = new ListNotification<Reply>("new-replies-notification", "replies", newReplyNotificationTemplate);
 
-               Template rescuingSonesTemplate = templateFactory.createTemplate(createReader("/templates/notify/rescuingSonesNotification.html"));
+               Template rescuingSonesTemplate = TemplateParser.parse(createReader("/templates/notify/rescuingSonesNotification.html"));
                rescuingSonesNotification = new ListNotification<Sone>("sones-being-rescued-notification", "sones", rescuingSonesTemplate);
 
-               Template sonesRescuedTemplate = templateFactory.createTemplate(createReader("/templates/notify/sonesRescuedNotification.html"));
+               Template sonesRescuedTemplate = TemplateParser.parse(createReader("/templates/notify/sonesRescuedNotification.html"));
                sonesRescuedNotification = new ListNotification<Sone>("sones-rescued-notification", "sones", sonesRescuedTemplate);
 
-               Template lockedSonesTemplate = templateFactory.createTemplate(createReader("/templates/notify/lockedSonesNotification.html"));
+               Template lockedSonesTemplate = TemplateParser.parse(createReader("/templates/notify/lockedSonesNotification.html"));
                lockedSonesNotification = new ListNotification<Sone>("sones-locked-notification", "sones", lockedSonesTemplate);
 
-               Template newVersionTemplate = templateFactory.createTemplate(createReader("/templates/notify/newVersionNotification.html"));
+               Template newVersionTemplate = TemplateParser.parse(createReader("/templates/notify/newVersionNotification.html"));
                newVersionNotification = new TemplateNotification("new-version-notification", newVersionTemplate);
        }
 
@@ -216,6 +251,15 @@ public class WebInterface implements CoreListener {
        }
 
        /**
+        * Returns the template context factory of the web interface.
+        *
+        * @return The template context factory
+        */
+       public TemplateContextFactory getTemplateContextFactory() {
+               return templateContextFactory;
+       }
+
+       /**
         * Returns the current session, creating a new session if there is no
         * current session.
         *
@@ -272,6 +316,10 @@ public class WebInterface implements CoreListener {
         *         currently logged in
         */
        public Sone getCurrentSone(ToadletContext toadletContext, boolean create) {
+               Set<Sone> localSones = getCore().getLocalSones();
+               if (localSones.size() == 1) {
+                       return localSones.iterator().next();
+               }
                return getCurrentSone(getCurrentSession(toadletContext, create));
        }
 
@@ -377,7 +425,7 @@ public class WebInterface implements CoreListener {
         */
        public void setFirstStart(boolean firstStart) {
                if (firstStart) {
-                       Template firstStartNotificationTemplate = templateFactory.createTemplate(createReader("/templates/notify/firstStartNotification.html"));
+                       Template firstStartNotificationTemplate = TemplateParser.parse(createReader("/templates/notify/firstStartNotification.html"));
                        Notification firstStartNotification = new TemplateNotification("first-start-notification", firstStartNotificationTemplate);
                        notificationManager.addNotification(firstStartNotification);
                }
@@ -392,7 +440,7 @@ public class WebInterface implements CoreListener {
         */
        public void setNewConfig(boolean newConfig) {
                if (newConfig && !hasFirstStartNotification()) {
-                       Template configNotReadNotificationTemplate = templateFactory.createTemplate(createReader("/templates/notify/configNotReadNotification.html"));
+                       Template configNotReadNotificationTemplate = TemplateParser.parse(createReader("/templates/notify/configNotReadNotification.html"));
                        Notification configNotReadNotification = new TemplateNotification("config-not-read-notification", configNotReadNotificationTemplate);
                        notificationManager.addNotification(configNotReadNotification);
                }
@@ -423,7 +471,7 @@ public class WebInterface implements CoreListener {
                registerToadlets();
 
                /* notification templates. */
-               Template startupNotificationTemplate = templateFactory.createTemplate(createReader("/templates/notify/startupNotification.html"));
+               Template startupNotificationTemplate = TemplateParser.parse(createReader("/templates/notify/startupNotification.html"));
 
                final TemplateNotification startupNotification = new TemplateNotification("startup-notification", startupNotificationTemplate);
                notificationManager.addNotification(startupNotification);
@@ -436,7 +484,7 @@ public class WebInterface implements CoreListener {
                        }
                }, "Sone Startup Notification Remover");
 
-               Template wotMissingNotificationTemplate = templateFactory.createTemplate(createReader("/templates/notify/wotMissingNotification.html"));
+               Template wotMissingNotificationTemplate = TemplateParser.parse(createReader("/templates/notify/wotMissingNotification.html"));
                final TemplateNotification wotMissingNotification = new TemplateNotification("wot-missing-notification", wotMissingNotificationTemplate);
                Ticker.getInstance().registerEvent(System.currentTimeMillis() + (15 * 1000), new Runnable() {
 
@@ -470,32 +518,38 @@ public class WebInterface implements CoreListener {
         * Register all toadlets.
         */
        private void registerToadlets() {
-               Template emptyTemplate = templateFactory.createTemplate(new StringReader(""));
-               Template loginTemplate = templateFactory.createTemplate(createReader("/templates/login.html"));
-               Template indexTemplate = templateFactory.createTemplate(createReader("/templates/index.html"));
-               Template knownSonesTemplate = templateFactory.createTemplate(createReader("/templates/knownSones.html"));
-               Template createSoneTemplate = templateFactory.createTemplate(createReader("/templates/createSone.html"));
-               Template createPostTemplate = templateFactory.createTemplate(createReader("/templates/createPost.html"));
-               Template createReplyTemplate = templateFactory.createTemplate(createReader("/templates/createReply.html"));
-               Template editProfileTemplate = templateFactory.createTemplate(createReader("/templates/editProfile.html"));
-               Template viewSoneTemplate = templateFactory.createTemplate(createReader("/templates/viewSone.html"));
-               Template viewPostTemplate = templateFactory.createTemplate(createReader("/templates/viewPost.html"));
-               Template deletePostTemplate = templateFactory.createTemplate(createReader("/templates/deletePost.html"));
-               Template deleteReplyTemplate = templateFactory.createTemplate(createReader("/templates/deleteReply.html"));
-               Template deleteSoneTemplate = templateFactory.createTemplate(createReader("/templates/deleteSone.html"));
-               Template imageBrowserTemplate = templateFactory.createTemplate(createReader("/templates/imageBrowser.html"));
-               Template createAlbumTemplate = templateFactory.createTemplate(createReader("/templates/createAlbum.html"));
-               Template noPermissionTemplate = templateFactory.createTemplate(createReader("/templates/noPermission.html"));
-               Template optionsTemplate = templateFactory.createTemplate(createReader("/templates/options.html"));
-               Template aboutTemplate = templateFactory.createTemplate(createReader("/templates/about.html"));
-               Template postTemplate = templateFactory.createTemplate(createReader("/templates/include/viewPost.html"));
-               Template replyTemplate = templateFactory.createTemplate(createReader("/templates/include/viewReply.html"));
+               Template emptyTemplate = TemplateParser.parse(new StringReader(""));
+               Template loginTemplate = TemplateParser.parse(createReader("/templates/login.html"));
+               Template indexTemplate = TemplateParser.parse(createReader("/templates/index.html"));
+               Template knownSonesTemplate = TemplateParser.parse(createReader("/templates/knownSones.html"));
+               Template createSoneTemplate = TemplateParser.parse(createReader("/templates/createSone.html"));
+               Template createPostTemplate = TemplateParser.parse(createReader("/templates/createPost.html"));
+               Template createReplyTemplate = TemplateParser.parse(createReader("/templates/createReply.html"));
+               Template bookmarksTemplate = TemplateParser.parse(createReader("/templates/bookmarks.html"));
+               Template editProfileTemplate = TemplateParser.parse(createReader("/templates/editProfile.html"));
+               Template editProfileFieldTemplate = TemplateParser.parse(createReader("/templates/editProfileField.html"));
+               Template deleteProfileFieldTemplate = TemplateParser.parse(createReader("/templates/deleteProfileField.html"));
+               Template viewSoneTemplate = TemplateParser.parse(createReader("/templates/viewSone.html"));
+               Template viewPostTemplate = TemplateParser.parse(createReader("/templates/viewPost.html"));
+               Template deletePostTemplate = TemplateParser.parse(createReader("/templates/deletePost.html"));
+               Template deleteReplyTemplate = TemplateParser.parse(createReader("/templates/deleteReply.html"));
+               Template deleteSoneTemplate = TemplateParser.parse(createReader("/templates/deleteSone.html"));
+               Template imageBrowserTemplate = TemplateParser.parse(createReader("/templates/imageBrowser.html"));
+               Template createAlbumTemplate = TemplateParser.parse(createReader("/templates/createAlbum.html"));
+               Template noPermissionTemplate = TemplateParser.parse(createReader("/templates/noPermission.html"));
+               Template optionsTemplate = TemplateParser.parse(createReader("/templates/options.html"));
+               Template aboutTemplate = TemplateParser.parse(createReader("/templates/about.html"));
+               Template invalidTemplate = TemplateParser.parse(createReader("/templates/invalid.html"));
+               Template postTemplate = TemplateParser.parse(createReader("/templates/include/viewPost.html"));
+               Template replyTemplate = TemplateParser.parse(createReader("/templates/include/viewReply.html"));
 
                PageToadletFactory pageToadletFactory = new PageToadletFactory(sonePlugin.pluginRespirator().getHLSimpleClient(), "/Sone/");
                pageToadlets.add(pageToadletFactory.createPageToadlet(new IndexPage(indexTemplate, this), "Index"));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new CreateSonePage(createSoneTemplate, this), "CreateSone"));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new KnownSonesPage(knownSonesTemplate, this), "KnownSones"));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new EditProfilePage(editProfileTemplate, this), "EditProfile"));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new EditProfileFieldPage(editProfileFieldTemplate, this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new DeleteProfileFieldPage(deleteProfileFieldTemplate, this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new CreatePostPage(createPostTemplate, this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new CreateReplyPage(createReplyTemplate, this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new ViewSonePage(viewSoneTemplate, this)));
@@ -510,6 +564,13 @@ public class WebInterface implements CoreListener {
                pageToadlets.add(pageToadletFactory.createPageToadlet(new UnfollowSonePage(emptyTemplate, this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new ImageBrowserPage(imageBrowserTemplate, this), "ImageBrowser"));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new CreateAlbumPage(createAlbumTemplate, this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new TrustPage(emptyTemplate, this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new DistrustPage(emptyTemplate, this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new UntrustPage(emptyTemplate, this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new MarkAsKnownPage(emptyTemplate, this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new BookmarkPage(emptyTemplate, this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new UnbookmarkPage(emptyTemplate, this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new BookmarksPage(bookmarksTemplate, this), "Bookmarks"));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new DeleteSonePage(deleteSoneTemplate, this), "DeleteSone"));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new LoginPage(loginTemplate, this), "Login"));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new LogoutPage(emptyTemplate, this), "Logout"));
@@ -517,6 +578,7 @@ public class WebInterface implements CoreListener {
                pageToadlets.add(pageToadletFactory.createPageToadlet(new AboutPage(aboutTemplate, this, SonePlugin.VERSION), "About"));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new SoneTemplatePage("noPermission.html", noPermissionTemplate, "Page.NoPermission.Title", this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new DismissNotificationPage(emptyTemplate, this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new SoneTemplatePage("invalid.html", invalidTemplate, "Page.Invalid.Title", this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new StaticPage("css/", "/static/css/", "text/css")));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new StaticPage("javascript/", "/static/javascript/", "text/javascript")));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new StaticPage("images/", "/static/images/", "image/png")));
@@ -527,17 +589,24 @@ public class WebInterface implements CoreListener {
                pageToadlets.add(pageToadletFactory.createPageToadlet(new CreateReplyAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new GetReplyAjaxPage(this, replyTemplate)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new GetPostAjaxPage(this, postTemplate)));
-               pageToadlets.add(pageToadletFactory.createPageToadlet(new MarkPostAsKnownPage(this)));
-               pageToadlets.add(pageToadletFactory.createPageToadlet(new MarkReplyAsKnownPage(this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new MarkAsKnownAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new DeletePostAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new DeleteReplyAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new LockSoneAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new UnlockSoneAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new FollowSoneAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new UnfollowSoneAjaxPage(this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new TrustAjaxPage(this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new DistrustAjaxPage(this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new UntrustAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new LikeAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new UnlikeAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new GetLikesAjaxPage(this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new BookmarkAjaxPage(this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new UnbookmarkAjaxPage(this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new EditProfileFieldAjaxPage(this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new DeleteProfileFieldAjaxPage(this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new MoveProfileFieldAjaxPage(this)));
 
                ToadletContainer toadletContainer = sonePlugin.pluginRespirator().getToadletContainer();
                toadletContainer.getPageMaker().addNavigationCategory("/Sone/index.html", "Navigation.Menu.Name", "Navigation.Menu.Tooltip", sonePlugin);
@@ -574,6 +643,7 @@ public class WebInterface implements CoreListener {
                try {
                        return new InputStreamReader(getClass().getResourceAsStream(resourceName), "UTF-8");
                } catch (UnsupportedEncodingException uee1) {
+                       System.out.println("  fail.");
                        return null;
                }
        }
@@ -712,9 +782,10 @@ public class WebInterface implements CoreListener {
         * {@inheritDoc}
         */
        @Override
-       public void updateFound(Version version, long releaseTime) {
-               newVersionNotification.set("version", version);
-               newVersionNotification.set("releaseTime", releaseTime);
+       public void updateFound(Version version, long releaseTime, long latestEdition) {
+               newVersionNotification.getTemplateContext().set("latestVersion", version);
+               newVersionNotification.getTemplateContext().set("latestEdition", latestEdition);
+               newVersionNotification.getTemplateContext().set("releaseTime", releaseTime);
                notificationManager.addNotification(newVersionNotification);
        }
 
@@ -725,36 +796,53 @@ public class WebInterface implements CoreListener {
         *
         * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
         */
-       private class ClassPathTemplateProvider implements TemplateProvider {
+       private class ClassPathTemplateProvider implements Provider {
 
-               /** The template factory. */
-               @SuppressWarnings("hiding")
-               private final TemplateFactory templateFactory;
+               /** Cache for templates. */
+               private final Cache<String, Template> templateCache = new MemoryCache<String, Template>(new ValueRetriever<String, Template>() {
+
+                       @Override
+                       @SuppressWarnings("synthetic-access")
+                       public CacheItem<Template> retrieve(String key) throws CacheException {
+                               Template template = findTemplate(key);
+                               if (template != null) {
+                                       return new DefaultCacheItem<Template>(template);
+                               }
+                               return null;
+                       }
+               });
 
                /**
-                * Creates a new template provider that locates templates on the
-                * classpath.
-                *
-                * @param templateFactory
-                *            The template factory to create the templates
+                * {@inheritDoc}
                 */
-               public ClassPathTemplateProvider(TemplateFactory templateFactory) {
-                       this.templateFactory = templateFactory;
+               @Override
+               @SuppressWarnings("synthetic-access")
+               public Template getTemplate(TemplateContext templateContext, String templateName) {
+                       try {
+                               return templateCache.get(templateName);
+                       } catch (CacheException ce1) {
+                               logger.log(Level.WARNING, "Could not get template for " + templateName + "!", ce1);
+                               return null;
+                       }
                }
 
                /**
-                * {@inheritDoc}
+                * Locates a template in the class path.
+                *
+                * @param templateName
+                *            The name of the template to load
+                * @return The loaded template, or {@code null} if no template could be
+                *         found
                 */
-               @Override
                @SuppressWarnings("synthetic-access")
-               public Template getTemplate(String templateName) {
+               private Template findTemplate(String templateName) {
                        Reader templateReader = createReader("/templates/" + templateName);
                        if (templateReader == null) {
                                return null;
                        }
-                       Template template = templateFactory.createTemplate(templateReader);
+                       Template template = null;
                        try {
-                               template.parse();
+                               template = TemplateParser.parse(templateReader);
                        } catch (TemplateException te1) {
                                logger.log(Level.WARNING, "Could not parse template “" + templateName + "” for inclusion!", te1);
                        }
diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/BookmarkAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/BookmarkAjaxPage.java
new file mode 100644 (file)
index 0000000..760bd34
--- /dev/null
@@ -0,0 +1,65 @@
+/*
+ * Sone - BookmarkAjaxPage.java - Copyright © 2011 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.ajax;
+
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.util.json.JsonObject;
+
+/**
+ * AJAX page that lets the user bookmark a post.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class BookmarkAjaxPage extends JsonPage {
+
+       /**
+        * Creates a new bookmark AJAX page.
+        *
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public BookmarkAjaxPage(WebInterface webInterface) {
+               super("bookmark.ajax", webInterface);
+       }
+
+       //
+       // JSONPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected JsonObject createJsonObject(Request request) {
+               String id = request.getHttpRequest().getParam("post", null);
+               if ((id == null) || (id.length() == 0)) {
+                       return createErrorJsonObject("invalid-post-id");
+               }
+               webInterface.getCore().bookmarkPost(id);
+               return createSuccessJsonObject();
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected boolean requiresLogin() {
+               return false;
+       }
+
+}
index 1e45a60..eff7c41 100644 (file)
@@ -50,12 +50,17 @@ public class CreatePostAjaxPage extends JsonPage {
                }
                String recipientId = request.getHttpRequest().getParam("recipient");
                Sone recipient = webInterface.getCore().getSone(recipientId, false);
+               String senderId = request.getHttpRequest().getParam("sender");
+               Sone sender = webInterface.getCore().getLocalSone(senderId, false);
+               if (sender == null) {
+                       sender = sone;
+               }
                String text = request.getHttpRequest().getParam("text");
                if ((text == null) || (text.trim().length() == 0)) {
                        return createErrorJsonObject("text-required");
                }
-               Post newPost = webInterface.getCore().createPost(sone, recipient, text);
-               return createSuccessJsonObject().put("postId", newPost.getId());
+               Post newPost = webInterface.getCore().createPost(sender, recipient, text);
+               return createSuccessJsonObject().put("postId", newPost.getId()).put("sone", sender.getId()).put("recipient", (newPost.getRecipient() != null) ? newPost.getRecipient().getId() : null);
        }
 
 }
index 0181ff2..9ed960f 100644 (file)
@@ -51,16 +51,17 @@ public class CreateReplyAjaxPage extends JsonPage {
        protected JsonObject createJsonObject(Request request) {
                String postId = request.getHttpRequest().getParam("post");
                String text = request.getHttpRequest().getParam("text").trim();
-               Sone currentSone = getCurrentSone(request.getToadletContext());
-               if (currentSone == null) {
-                       return createErrorJsonObject("auth-required");
+               String senderId = request.getHttpRequest().getParam("sender");
+               Sone sender = webInterface.getCore().getLocalSone(senderId, false);
+               if (sender == null) {
+                       sender = getCurrentSone(request.getToadletContext());
                }
                Post post = webInterface.getCore().getPost(postId);
                if ((post == null) || (post.getSone() == null)) {
                        return createErrorJsonObject("invalid-post-id");
                }
-               Reply reply = webInterface.getCore().createReply(currentSone, post, text);
-               return createSuccessJsonObject().put("reply", reply.getId());
+               Reply reply = webInterface.getCore().createReply(sender, post, text);
+               return createSuccessJsonObject().put("reply", reply.getId()).put("sone", sender.getId());
        }
 
 }
index 479e71e..8d3b414 100644 (file)
@@ -18,7 +18,6 @@
 package net.pterodactylus.sone.web.ajax;
 
 import net.pterodactylus.sone.data.Post;
-import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.web.WebInterface;
 import net.pterodactylus.util.json.JsonObject;
 
@@ -50,14 +49,10 @@ public class DeletePostAjaxPage extends JsonPage {
        protected JsonObject createJsonObject(Request request) {
                String postId = request.getHttpRequest().getParam("post");
                Post post = webInterface.getCore().getPost(postId, false);
-               Sone currentSone = getCurrentSone(request.getToadletContext());
                if ((post == null) || (post.getSone() == null)) {
                        return createErrorJsonObject("invalid-post-id");
                }
-               if (currentSone == null) {
-                       return createErrorJsonObject("auth-required");
-               }
-               if (!post.getSone().equals(currentSone)) {
+               if (!webInterface.getCore().isLocalSone(post.getSone())) {
                        return createErrorJsonObject("not-authorized");
                }
                webInterface.getCore().deletePost(post);
diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/DeleteProfileFieldAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/DeleteProfileFieldAjaxPage.java
new file mode 100644 (file)
index 0000000..383d2d4
--- /dev/null
@@ -0,0 +1,61 @@
+/*
+ * Sone - DeleteProfileFieldAjaxPage.java - Copyright © 2011 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.ajax;
+
+import net.pterodactylus.sone.data.Profile;
+import net.pterodactylus.sone.data.Profile.Field;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.util.json.JsonObject;
+
+/**
+ * AJAX page that lets the user delete a profile field.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class DeleteProfileFieldAjaxPage extends JsonPage {
+
+       /**
+        * Creates a new “delete profile field” AJAX page.
+        *
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public DeleteProfileFieldAjaxPage(WebInterface webInterface) {
+               super("deleteProfileField.ajax", webInterface);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected JsonObject createJsonObject(Request request) {
+               String fieldId = request.getHttpRequest().getParam("field");
+               Sone currentSone = getCurrentSone(request.getToadletContext());
+               Profile profile = currentSone.getProfile();
+               Field field = profile.getFieldById(fieldId);
+               if (field == null) {
+                       return createErrorJsonObject("invalid-field-id");
+               }
+               profile.removeField(field);
+               currentSone.setProfile(profile);
+               webInterface.getCore().saveSone(currentSone);
+               return createSuccessJsonObject().put("field", new JsonObject().put("id", field.getId()));
+       }
+
+}
index 7614de8..f34d202 100644 (file)
@@ -18,7 +18,6 @@
 package net.pterodactylus.sone.web.ajax;
 
 import net.pterodactylus.sone.data.Reply;
-import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.web.WebInterface;
 import net.pterodactylus.util.json.JsonObject;
 
@@ -50,14 +49,10 @@ public class DeleteReplyAjaxPage extends JsonPage {
        protected JsonObject createJsonObject(Request request) {
                String replyId = request.getHttpRequest().getParam("reply");
                Reply reply = webInterface.getCore().getReply(replyId);
-               Sone currentSone = getCurrentSone(request.getToadletContext());
                if (reply == null) {
                        return createErrorJsonObject("invalid-reply-id");
                }
-               if (currentSone == null) {
-                       return createErrorJsonObject("auth-required");
-               }
-               if (!reply.getSone().equals(currentSone)) {
+               if (!webInterface.getCore().isLocalSone(reply.getSone())) {
                        return createErrorJsonObject("not-authorized");
                }
                webInterface.getCore().deleteReply(reply);
index 283e924..08e3ee5 100644 (file)
@@ -55,4 +55,12 @@ public class DismissNotificationAjaxPage extends JsonPage {
                return createSuccessJsonObject();
        }
 
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected boolean requiresLogin() {
+               return false;
+       }
+
 }
diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/DistrustAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/DistrustAjaxPage.java
new file mode 100644 (file)
index 0000000..1a6d28f
--- /dev/null
@@ -0,0 +1,66 @@
+/*
+ * Sone - TrustAjaxPage.java - Copyright © 2011 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.ajax;
+
+import net.pterodactylus.sone.core.Core;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.freenet.wot.Trust;
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.util.json.JsonObject;
+
+/**
+ * AJAX page that lets the user distrust a Sone.
+ *
+ * @see Core#distrustSone(Sone, Sone)
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class DistrustAjaxPage extends JsonPage {
+
+       /**
+        * Creates a new “distrust Sone” AJAX handler.
+        *
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public DistrustAjaxPage(WebInterface webInterface) {
+               super("distrustSone.ajax", webInterface);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected JsonObject createJsonObject(Request request) {
+               Sone currentSone = getCurrentSone(request.getToadletContext(), false);
+               if (currentSone == null) {
+                       return createErrorJsonObject("auth-required");
+               }
+               String soneId = request.getHttpRequest().getParam("sone");
+               Sone sone = webInterface.getCore().getSone(soneId, false);
+               if (sone == null) {
+                       return createErrorJsonObject("invalid-sone-id");
+               }
+               webInterface.getCore().distrustSone(currentSone, sone);
+               Trust trust = webInterface.getCore().getTrust(currentSone, sone);
+               if (trust == null) {
+                       return createErrorJsonObject("wot-plugin");
+               }
+               return createSuccessJsonObject().put("trustValue", trust.getExplicit());
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/EditProfileFieldAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/EditProfileFieldAjaxPage.java
new file mode 100644 (file)
index 0000000..0036545
--- /dev/null
@@ -0,0 +1,72 @@
+/*
+ * Sone - EditProfileFieldAjaxPage.java - Copyright © 2011 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.ajax;
+
+import net.pterodactylus.sone.data.Profile;
+import net.pterodactylus.sone.data.Profile.Field;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.util.json.JsonObject;
+
+/**
+ * AJAX page that lets the user rename a profile field.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class EditProfileFieldAjaxPage extends JsonPage {
+
+       /**
+        * Creates a new “edit profile field” AJAX page.
+        *
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public EditProfileFieldAjaxPage(WebInterface webInterface) {
+               super("editProfileField.ajax", webInterface);
+       }
+
+       //
+       // JSONPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected JsonObject createJsonObject(Request request) {
+               String fieldId = request.getHttpRequest().getParam("field");
+               Sone currentSone = getCurrentSone(request.getToadletContext());
+               Profile profile = currentSone.getProfile();
+               Field field = profile.getFieldById(fieldId);
+               if (field == null) {
+                       return createErrorJsonObject("invalid-field-id");
+               }
+               String name = request.getHttpRequest().getParam("name", "").trim();
+               if (name.length() == 0) {
+                       return createErrorJsonObject("invalid-parameter-name");
+               }
+               Field existingField = profile.getFieldByName(name);
+               if ((existingField != null) && !existingField.equals(field)) {
+                       return createErrorJsonObject("duplicate-field-name");
+               }
+               field.setName(name);
+               currentSone.setProfile(profile);
+               return createSuccessJsonObject();
+       }
+
+}
index 92d5da6..4e5f1b8 100644 (file)
@@ -24,8 +24,8 @@ import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.web.WebInterface;
 import net.pterodactylus.util.io.Closer;
 import net.pterodactylus.util.json.JsonObject;
-import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
 import net.pterodactylus.util.template.TemplateException;
 
 /**
@@ -94,11 +94,12 @@ public class GetPostAjaxPage extends JsonPage {
                jsonPost.put("recipient", (post.getRecipient() == null) ? null : post.getRecipient().getId());
                jsonPost.put("time", post.getTime());
                StringWriter stringWriter = new StringWriter();
-               DataProvider dataProvider = postTemplate.createDataProvider();
-               dataProvider.setData("post", post);
-               dataProvider.setData("currentSone", currentSone);
+               TemplateContext templateContext = webInterface.getTemplateContextFactory().createTemplateContext();
+               templateContext.set("post", post);
+               templateContext.set("currentSone", currentSone);
+               templateContext.set("localSones", webInterface.getCore().getLocalSones());
                try {
-                       postTemplate.render(dataProvider, stringWriter);
+                       postTemplate.render(templateContext, stringWriter);
                } catch (TemplateException te1) {
                        /* TODO - shouldn’t happen. */
                } finally {
index e589191..c19bcba 100644 (file)
@@ -24,8 +24,8 @@ import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.web.WebInterface;
 import net.pterodactylus.util.io.Closer;
 import net.pterodactylus.util.json.JsonObject;
-import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
 import net.pterodactylus.util.template.TemplateException;
 
 /**
@@ -96,11 +96,11 @@ public class GetReplyAjaxPage extends JsonPage {
                jsonReply.put("soneId", reply.getSone().getId());
                jsonReply.put("time", reply.getTime());
                StringWriter stringWriter = new StringWriter();
-               DataProvider dataProvider = replyTemplate.createDataProvider();
-               dataProvider.setData("reply", reply);
-               dataProvider.setData("currentSone", currentSone);
+               TemplateContext templateContext = webInterface.getTemplateContextFactory().createTemplateContext();
+               templateContext.set("reply", reply);
+               templateContext.set("currentSone", currentSone);
                try {
-                       replyTemplate.render(dataProvider, stringWriter);
+                       replyTemplate.render(templateContext, stringWriter);
                } catch (TemplateException te1) {
                        /* TODO - shouldn’t happen. */
                } finally {
index 015add5..fd9ae76 100644 (file)
@@ -17,6 +17,8 @@
 
 package net.pterodactylus.sone.web.ajax;
 
+import java.io.IOException;
+import java.io.StringWriter;
 import java.text.DateFormat;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
@@ -34,6 +36,8 @@ import net.pterodactylus.sone.web.WebInterface;
 import net.pterodactylus.util.json.JsonArray;
 import net.pterodactylus.util.json.JsonObject;
 import net.pterodactylus.util.notify.Notification;
+import net.pterodactylus.util.notify.TemplateNotification;
+import net.pterodactylus.util.template.TemplateContext;
 
 /**
  * The “get status” AJAX handler returns all information that is necessary to
@@ -91,13 +95,23 @@ public class GetStatusAjaxPage extends JsonPage {
                Set<Post> newPosts = webInterface.getNewPosts();
                JsonArray jsonPosts = new JsonArray();
                for (Post post : newPosts) {
-                       jsonPosts.add(post.getId());
+                       JsonObject jsonPost = new JsonObject();
+                       jsonPost.put("id", post.getId());
+                       jsonPost.put("sone", post.getSone().getId());
+                       jsonPost.put("recipient", (post.getRecipient() != null) ? post.getRecipient().getId() : null);
+                       jsonPost.put("time", post.getTime());
+                       jsonPosts.add(jsonPost);
                }
                /* load new replies. */
                Set<Reply> newReplies = webInterface.getNewReplies();
                JsonArray jsonReplies = new JsonArray();
                for (Reply reply : newReplies) {
-                       jsonReplies.add(reply.getId());
+                       JsonObject jsonReply = new JsonObject();
+                       jsonReply.put("id", reply.getId());
+                       jsonReply.put("sone", reply.getSone().getId());
+                       jsonReply.put("post", reply.getPost().getId());
+                       jsonReply.put("postSone", reply.getPost().getSone().getId());
+                       jsonReplies.add(jsonReply);
                }
                return createSuccessJsonObject().put("sones", jsonSones).put("notifications", jsonNotifications).put("removedNotifications", jsonRemovedNotifications).put("newPosts", jsonPosts).put("newReplies", jsonReplies);
        }
@@ -110,6 +124,14 @@ public class GetStatusAjaxPage extends JsonPage {
                return false;
        }
 
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected boolean requiresLogin() {
+               return false;
+       }
+
        //
        // PRIVATE METHODS
        //
@@ -129,6 +151,7 @@ public class GetStatusAjaxPage extends JsonPage {
                jsonSone.put("status", webInterface.getCore().getSoneStatus(sone).name());
                jsonSone.put("modified", webInterface.getCore().isModifiedSone(sone));
                jsonSone.put("locked", webInterface.getCore().isLocked(sone));
+               jsonSone.put("lastUpdatedUnknown", sone.getTime() == 0);
                synchronized (dateFormat) {
                        jsonSone.put("lastUpdated", dateFormat.format(new Date(sone.getTime())));
                }
@@ -143,10 +166,22 @@ public class GetStatusAjaxPage extends JsonPage {
         *            The notification to create a JSON object
         * @return The JSON object
         */
-       private static JsonObject createJsonNotification(Notification notification) {
+       private JsonObject createJsonNotification(Notification notification) {
                JsonObject jsonNotification = new JsonObject();
                jsonNotification.put("id", notification.getId());
-               jsonNotification.put("text", notification.toString());
+               StringWriter notificationWriter = new StringWriter();
+               try {
+                       if (notification instanceof TemplateNotification) {
+                               TemplateContext templateContext = webInterface.getTemplateContextFactory().createTemplateContext().mergeContext(((TemplateNotification) notification).getTemplateContext());
+                               templateContext.set("notification", notification);
+                               ((TemplateNotification) notification).render(templateContext, notificationWriter);
+                       } else {
+                               notification.render(notificationWriter);
+                       }
+               } catch (IOException ioe1) {
+                       /* StringWriter never throws, ignore. */
+               }
+               jsonNotification.put("text", notificationWriter.toString());
                jsonNotification.put("createdTime", notification.getCreatedTime());
                jsonNotification.put("lastUpdatedTime", notification.getLastUpdatedTime());
                jsonNotification.put("dismissable", notification.isDismissable());
index 36327ca..725b13a 100644 (file)
@@ -59,4 +59,12 @@ public class GetTranslationPage extends JsonPage {
                return false;
        }
 
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected boolean requiresLogin() {
+               return false;
+       }
+
 }
index 605afaf..8d48bce 100644 (file)
@@ -137,6 +137,16 @@ public abstract class JsonPage implements Page {
                return true;
        }
 
+       /**
+        * Returns whether this page requires the user to be logged in.
+        *
+        * @return {@code true} if the user needs to be logged in to use this page,
+        *         {@code false} otherwise
+        */
+       protected boolean requiresLogin() {
+               return true;
+       }
+
        //
        // PROTECTED METHODS
        //
@@ -184,6 +194,11 @@ public abstract class JsonPage implements Page {
                                return new Response(401, "Not authorized", "application/json", JsonUtils.format(new JsonObject().put("success", false).put("error", "auth-required")));
                        }
                }
+               if (requiresLogin()) {
+                       if (getCurrentSone(request.getToadletContext(), false) == null) {
+                               return new Response(401, "Not authorized", "application/json", JsonUtils.format(createErrorJsonObject("auth-required")));
+                       }
+               }
                JsonObject jsonObject = createJsonObject(request);
                return new Response(200, "OK", "application/json", JsonUtils.format(jsonObject));
        }
index 6c4ece0..bca9a35 100644 (file)
@@ -53,4 +53,12 @@ public class LockSoneAjaxPage extends JsonPage {
                return createSuccessJsonObject();
        }
 
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected boolean requiresLogin() {
+               return false;
+       }
+
 }
diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/MarkAsKnownAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/MarkAsKnownAjaxPage.java
new file mode 100644 (file)
index 0000000..b641ea0
--- /dev/null
@@ -0,0 +1,88 @@
+/*
+ * Sone - MarkAsKnownAjaxPage.java - Copyright © 2011 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.ajax;
+
+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.web.WebInterface;
+import net.pterodactylus.util.json.JsonObject;
+
+/**
+ * AJAX page that lets the user mark a number of {@link Sone}s, {@link Post}s,
+ * or {@link Reply}s as known.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class MarkAsKnownAjaxPage extends JsonPage {
+
+       /**
+        * Creates a new “mark as known” AJAX page.
+        *
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public MarkAsKnownAjaxPage(WebInterface webInterface) {
+               super("markAsKnown.ajax", webInterface);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected JsonObject createJsonObject(Request request) {
+               String type = request.getHttpRequest().getParam("type");
+               if (!type.equals("sone") && !type.equals("post") && !type.equals("reply")) {
+                       return createErrorJsonObject("invalid-type");
+               }
+               String[] ids = request.getHttpRequest().getParam("id").split(" ");
+               Core core = webInterface.getCore();
+               for (String id : ids) {
+                       if (type.equals("post")) {
+                               Post post = core.getPost(id, false);
+                               if (post == null) {
+                                       continue;
+                               }
+                               core.markPostKnown(post);
+                       } else if (type.equals("reply")) {
+                               Reply reply = core.getReply(id, false);
+                               if (reply == null) {
+                                       continue;
+                               }
+                               core.markReplyKnown(reply);
+                       } else if (type.equals("sone")) {
+                               Sone sone = core.getSone(id, false);
+                               if (sone == null) {
+                                       continue;
+                               }
+                               core.markSoneKnown(sone);
+                       }
+               }
+               return createSuccessJsonObject();
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected boolean requiresLogin() {
+               return false;
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/MarkPostAsKnownPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/MarkPostAsKnownPage.java
deleted file mode 100644 (file)
index c2b0ca3..0000000
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Sone - MarkPostAsKnownPage.java - Copyright © 2010 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.web.ajax;
-
-import net.pterodactylus.sone.data.Post;
-import net.pterodactylus.sone.web.WebInterface;
-import net.pterodactylus.util.json.JsonObject;
-
-/**
- * AJAX handler that marks a {@link Post} as known.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class MarkPostAsKnownPage extends JsonPage {
-
-       /**
-        * Creates a new “mark post as known” AJAX handler.
-        *
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public MarkPostAsKnownPage(WebInterface webInterface) {
-               super("markPostAsKnown.ajax", webInterface);
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected JsonObject createJsonObject(Request request) {
-               String postId = request.getHttpRequest().getParam("post");
-               Post post = webInterface.getCore().getPost(postId, false);
-               if (post == null) {
-                       return createErrorJsonObject("invalid-post-id");
-               }
-               webInterface.getCore().markPostKnown(post);
-               return createSuccessJsonObject();
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/MarkReplyAsKnownPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/MarkReplyAsKnownPage.java
deleted file mode 100644 (file)
index 6a4f72b..0000000
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Sone - MarkPostAsKnownPage.java - Copyright © 2010 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.web.ajax;
-
-import net.pterodactylus.sone.data.Reply;
-import net.pterodactylus.sone.web.WebInterface;
-import net.pterodactylus.util.json.JsonObject;
-
-/**
- * AJAX handler that marks a {@link Reply} as known.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class MarkReplyAsKnownPage extends JsonPage {
-
-       /**
-        * Creates a new “mark reply as known” AJAX handler.
-        *
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public MarkReplyAsKnownPage(WebInterface webInterface) {
-               super("markReplyAsKnown.ajax", webInterface);
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected JsonObject createJsonObject(Request request) {
-               String replyId = request.getHttpRequest().getParam("reply");
-               Reply reply = webInterface.getCore().getReply(replyId, false);
-               if (reply == null) {
-                       return createErrorJsonObject("invalid-reply-id");
-               }
-               webInterface.getCore().markReplyKnown(reply);
-               return createSuccessJsonObject();
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/MoveProfileFieldAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/MoveProfileFieldAjaxPage.java
new file mode 100644 (file)
index 0000000..780e4d1
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+ * Sone - MoveProfileFieldAjaxPage.java - Copyright © 2011 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.ajax;
+
+import net.pterodactylus.sone.data.Profile;
+import net.pterodactylus.sone.data.Profile.Field;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.util.json.JsonObject;
+
+/**
+ * AJAX page that lets the user move a profile field up or down.
+ *
+ * @see Profile#moveFieldUp(Field)
+ * @see Profile#moveFieldDown(Field)
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class MoveProfileFieldAjaxPage extends JsonPage {
+
+       /**
+        * Creates a new “move profile field” AJAX page.
+        *
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public MoveProfileFieldAjaxPage(WebInterface webInterface) {
+               super("moveProfileField.ajax", webInterface);
+       }
+
+       //
+       // JSONPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected JsonObject createJsonObject(Request request) {
+               Sone currentSone = getCurrentSone(request.getToadletContext());
+               Profile profile = currentSone.getProfile();
+               String fieldId = request.getHttpRequest().getParam("field");
+               Field field = profile.getFieldById(fieldId);
+               if (field == null) {
+                       return createErrorJsonObject("invalid-field-id");
+               }
+               String direction = request.getHttpRequest().getParam("direction");
+               try {
+                       if ("up".equals(direction)) {
+                               profile.moveFieldUp(field);
+                       } else if ("down".equals(direction)) {
+                               profile.moveFieldDown(field);
+                       } else {
+                               return createErrorJsonObject("invalid-direction");
+                       }
+               } catch (IllegalArgumentException iae1) {
+                       return createErrorJsonObject("not-possible");
+               }
+               currentSone.setProfile(profile);
+               webInterface.getCore().saveSone(currentSone);
+               return createSuccessJsonObject();
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/TrustAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/TrustAjaxPage.java
new file mode 100644 (file)
index 0000000..d6e2750
--- /dev/null
@@ -0,0 +1,66 @@
+/*
+ * Sone - TrustAjaxPage.java - Copyright © 2011 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.ajax;
+
+import net.pterodactylus.sone.core.Core;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.freenet.wot.Trust;
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.util.json.JsonObject;
+
+/**
+ * AJAX page that lets the user trust a Sone.
+ *
+ * @see Core#trustSone(Sone, Sone)
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class TrustAjaxPage extends JsonPage {
+
+       /**
+        * Creates a new “trust Sone” AJAX handler.
+        *
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public TrustAjaxPage(WebInterface webInterface) {
+               super("trustSone.ajax", webInterface);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected JsonObject createJsonObject(Request request) {
+               Sone currentSone = getCurrentSone(request.getToadletContext(), false);
+               if (currentSone == null) {
+                       return createErrorJsonObject("auth-required");
+               }
+               String soneId = request.getHttpRequest().getParam("sone");
+               Sone sone = webInterface.getCore().getSone(soneId, false);
+               if (sone == null) {
+                       return createErrorJsonObject("invalid-sone-id");
+               }
+               webInterface.getCore().trustSone(currentSone, sone);
+               Trust trust = webInterface.getCore().getTrust(currentSone, sone);
+               if (trust == null) {
+                       return createErrorJsonObject("wot-plugin");
+               }
+               return createSuccessJsonObject().put("trustValue", trust.getExplicit());
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/UnbookmarkAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/UnbookmarkAjaxPage.java
new file mode 100644 (file)
index 0000000..ad1689e
--- /dev/null
@@ -0,0 +1,65 @@
+/*
+ * Sone - BookmarkAjaxPage.java - Copyright © 2011 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.ajax;
+
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.util.json.JsonObject;
+
+/**
+ * AJAX page that lets the user unbookmark a post.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class UnbookmarkAjaxPage extends JsonPage {
+
+       /**
+        * Creates a new unbookmark AJAX page.
+        *
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public UnbookmarkAjaxPage(WebInterface webInterface) {
+               super("unbookmark.ajax", webInterface);
+       }
+
+       //
+       // JSONPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected JsonObject createJsonObject(Request request) {
+               String id = request.getHttpRequest().getParam("post", null);
+               if ((id == null) || (id.length() == 0)) {
+                       return createErrorJsonObject("invalid-post-id");
+               }
+               webInterface.getCore().unbookmarkPost(id);
+               return createSuccessJsonObject();
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected boolean requiresLogin() {
+               return false;
+       }
+
+}
index 02682b2..e1c408c 100644 (file)
@@ -53,4 +53,12 @@ public class UnlockSoneAjaxPage extends JsonPage {
                return createSuccessJsonObject();
        }
 
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected boolean requiresLogin() {
+               return false;
+       }
+
 }
diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/UntrustAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/UntrustAjaxPage.java
new file mode 100644 (file)
index 0000000..32c6229
--- /dev/null
@@ -0,0 +1,66 @@
+/*
+ * Sone - TrustAjaxPage.java - Copyright © 2011 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.ajax;
+
+import net.pterodactylus.sone.core.Core;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.freenet.wot.Trust;
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.util.json.JsonObject;
+
+/**
+ * AJAX page that lets the user untrust a Sone.
+ *
+ * @see Core#untrustSone(Sone, Sone)
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class UntrustAjaxPage extends JsonPage {
+
+       /**
+        * Creates a new “untrust Sone” AJAX handler.
+        *
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public UntrustAjaxPage(WebInterface webInterface) {
+               super("untrustSone.ajax", webInterface);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected JsonObject createJsonObject(Request request) {
+               Sone currentSone = getCurrentSone(request.getToadletContext(), false);
+               if (currentSone == null) {
+                       return createErrorJsonObject("auth-required");
+               }
+               String soneId = request.getHttpRequest().getParam("sone");
+               Sone sone = webInterface.getCore().getSone(soneId, false);
+               if (sone == null) {
+                       return createErrorJsonObject("invalid-sone-id");
+               }
+               webInterface.getCore().untrustSone(currentSone, sone);
+               Trust trust = webInterface.getCore().getTrust(currentSone, sone);
+               if (trust == null) {
+                       return createErrorJsonObject("wot-plugin");
+               }
+               return createSuccessJsonObject().put("trustValue", trust.getExplicit());
+       }
+
+}
index e6ee539..9b40deb 100644 (file)
@@ -25,8 +25,9 @@ import java.util.logging.Logger;
 
 import net.pterodactylus.sone.web.page.Page.Request.Method;
 import net.pterodactylus.util.logging.Logging;
-import net.pterodactylus.util.template.DataProvider;
 import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.template.TemplateContextFactory;
 import freenet.clients.http.LinkEnabledCallback;
 import freenet.clients.http.PageMaker;
 import freenet.clients.http.PageNode;
@@ -46,6 +47,9 @@ public class TemplatePage implements Page, LinkEnabledCallback {
        /** The path of the page. */
        private final String path;
 
+       /** The template context factory. */
+       private final TemplateContextFactory templateContextFactory;
+
        /** The template to render. */
        private final Template template;
 
@@ -63,6 +67,8 @@ public class TemplatePage implements Page, LinkEnabledCallback {
         *
         * @param path
         *            The path of the page
+        * @param templateContextFactory
+        *            The template context factory
         * @param template
         *            The template to render
         * @param l10n
@@ -73,8 +79,9 @@ public class TemplatePage implements Page, LinkEnabledCallback {
         *            The target to redirect to if a POST request does not contain
         *            the correct form password
         */
-       public TemplatePage(String path, Template template, BaseL10n l10n, String pageTitleKey, String invalidFormPasswordRedirectTarget) {
+       public TemplatePage(String path, TemplateContextFactory templateContextFactory, Template template, BaseL10n l10n, String pageTitleKey, String invalidFormPasswordRedirectTarget) {
                this.path = path;
+               this.templateContextFactory = templateContextFactory;
                this.template = template;
                this.l10n = l10n;
                this.pageTitleKey = pageTitleKey;
@@ -117,10 +124,11 @@ public class TemplatePage implements Page, LinkEnabledCallback {
                        pageNode.addForwardLink("icon", shortcutIcon);
                }
 
-               DataProvider dataProvider = template.createDataProvider();
+               TemplateContext templateContext = templateContextFactory.createTemplateContext();
+               templateContext.mergeContext(template.getInitialContext());
                try {
                        long start = System.nanoTime();
-                       processTemplate(request, dataProvider);
+                       processTemplate(request, templateContext);
                        long finish = System.nanoTime();
                        logger.log(Level.FINEST, "Template was rendered in " + ((finish - start) / 1000) / 1000.0 + "ms.");
                } catch (RedirectException re1) {
@@ -128,10 +136,10 @@ public class TemplatePage implements Page, LinkEnabledCallback {
                }
 
                StringWriter stringWriter = new StringWriter();
-               template.render(dataProvider, stringWriter);
+               template.render(templateContext, stringWriter);
                pageNode.content.addChild("%", stringWriter.toString());
 
-               postProcess(request, dataProvider);
+               postProcess(request, templateContext);
 
                return new Response(200, "OK", "text/html", pageNode.outer.generate());
        }
@@ -161,29 +169,29 @@ public class TemplatePage implements Page, LinkEnabledCallback {
         *
         * @param request
         *            The request that is rendered
-        * @param dataProvider
-        *            The data provider to set variables in
+        * @param templateContext
+        *            The template context to set variables in
         * @throws RedirectException
         *             if the processing page wants to redirect after processing
         */
-       protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
+       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
                /* do nothing. */
        }
 
        /**
         * This method will be called after
-        * {@link #processTemplate(net.pterodactylus.sone.web.page.Page.Request, DataProvider)}
+        * {@link #processTemplate(net.pterodactylus.sone.web.page.Page.Request, TemplateContext)}
         * has processed the template and the template was rendered. This method
         * will not be called if
-        * {@link #processTemplate(net.pterodactylus.sone.web.page.Page.Request, DataProvider)}
+        * {@link #processTemplate(net.pterodactylus.sone.web.page.Page.Request, TemplateContext)}
         * throws a {@link RedirectException}!
         *
         * @param request
         *            The request being processed
-        * @param dataProvider
-        *            The data provider that supplied the rendered data
+        * @param templateContext
+        *            The template context that supplied the rendered data
         */
-       protected void postProcess(Request request, DataProvider dataProvider) {
+       protected void postProcess(Request request, TemplateContext templateContext) {
                /* do nothing. */
        }
 
@@ -214,7 +222,7 @@ public class TemplatePage implements Page, LinkEnabledCallback {
        /**
         * Exception that can be thrown to signal that a subclassed {@link Page}
         * wants to redirect the user during the
-        * {@link TemplatePage#processTemplate(net.pterodactylus.sone.web.page.Page.Request, DataProvider)}
+        * {@link TemplatePage#processTemplate(net.pterodactylus.sone.web.page.Page.Request, TemplateContext)}
         * method call.
         *
         * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
index 2c15817..3f64f51 100644 (file)
@@ -8,6 +8,8 @@ Navigation.Menu.Item.CreateSone.Name=Create Sone
 Navigation.Menu.Item.CreateSone.Tooltip=Create a new Sone
 Navigation.Menu.Item.KnownSones.Name=Known Sones
 Navigation.Menu.Item.KnownSones.Tooltip=Shows all known Sones
+Navigation.Menu.Item.Bookmarks.Name=Bookmarks
+Navigation.Menu.Item.Bookmarks.Tooltip=Show bookmarked posts
 Navigation.Menu.Item.EditProfile.Name=Edit Profile
 Navigation.Menu.Item.EditProfile.Tooltip=Edit the Profile of your Sone
 Navigation.Menu.Item.ImageBrowser.Name=Images
@@ -29,6 +31,10 @@ Page.Options.Page.Title=Options
 Page.Options.Page.Description=These options influence the runtime behaviour of the Sone plugin.
 Page.Options.Section.RuntimeOptions.Title=Runtime Behaviour
 Page.Options.Option.InsertionDelay.Description=The number of seconds the Sone inserter waits after a modification of a Sone before it is being inserted.
+Page.Options.Section.TrustOptions.Title=Trust Settings
+Page.Options.Option.PositiveTrust.Description=The amount of positive trust you want to assign to other Sones by clicking the checkmark below a post or reply.
+Page.Options.Option.NegativeTrust.Description=The amount of trust you want to assign to other Sones by clicking the red X below a post or reply. This value should be negative.
+Page.Options.Option.TrustComment.Description=The comment that will be set in the web of trust for any trust you assign from Sone.
 Page.Options.Section.RescueOptions.Title=Rescue Settings
 Page.Options.Option.SoneRescueMode.Description=Try to rescue your Sones at the next start of the Sone plugin. This will read your all your old Sones from Freenet and ignore any disappearing postings and replies. You have to unlock your local Sones after they have been restored and you have to manually disable the rescue mode once you are satisfied with what has been restored!
 Page.Options.Section.Cleaning.Title=Clean Up
@@ -53,6 +59,7 @@ Page.DeleteSone.Button.No=No, do not delete.
 
 Page.Index.Title=Your Sone - Sone
 Page.Index.Label.Text=Post text:
+Page.Index.Label.Sender=Sender:
 Page.Index.Button.Post=Post!
 Page.Index.PostList.Title=Post Feed
 Page.Index.PostList.Text.NoPostYet=Nobody has written any posts yet. You should probably start it right now!
@@ -72,8 +79,32 @@ Page.EditProfile.Birthday.Title=Birthday
 Page.EditProfile.Birthday.Label.Day=Day:
 Page.EditProfile.Birthday.Label.Month=Month:
 Page.EditProfile.Birthday.Label.Year=Year:
-Page.EditProfile.Page.Status.Changed=Your changes have been saved and will be inserted shortly.
+Page.EditProfile.Fields.Title=Custom Fields
+Page.EditProfile.Fields.Description=Here you can enter custom fields into your profile. These fields can contain anything you want and be as terse or as verbose as you wish. Just remember that when it comes to anonymity, sometimes less is more.
+Page.EditProfile.Fields.Button.Edit=edit
+Page.EditProfile.Fields.Button.MoveUp=move up
+Page.EditProfile.Fields.Button.MoveDown=move down
+Page.EditProfile.Fields.Button.Delete=delete
+Page.EditProfile.Fields.Button.ReallyDelete=really delete
+Page.EditProfile.Fields.AddField.Title=Add Field
+Page.EditProfile.Fields.AddField.Label.Name=Name:
+Page.EditProfile.Fields.AddField.Button.AddField=Add Field
 Page.EditProfile.Button.Save=Save Profile
+Page.EditProfile.Error.DuplicateFieldName=The field name “{fieldName}” does already exist.
+
+Page.EditProfileField.Title=Edit Profile Field - Sone
+Page.EditProfileField.Page.Title=Edit Profile Field
+Page.EditProfileField.Text=Enter a new name for this profile field.
+Page.EditProfileField.Error.DuplicateFieldName=The field name you entered does already exist.
+Page.EditProfileField.Button.Save=Change
+Page.EditProfileField.Button.Reset=Revert to old name
+Page.EditProfileField.Button.Cancel=Do not change name
+
+Page.DeleteProfileField.Title=Delete Profile Field - Sone
+Page.DeleteProfileField.Page.Title=Delete Profile Field
+Page.DeleteProfileField.Text=Do you really want to delete this profile field?
+Page.DeleteProfileField.Button.Yes=Yes, delete
+Page.DeleteProfileField.Button.No=No, do not delete
 
 Page.CreatePost.Title=Create Post - Sone
 Page.CreatePost.Page.Title=Create Post
@@ -94,6 +125,8 @@ Page.ViewSone.UnknownSone.Description=This Sone has not yet been retrieved. Plea
 Page.ViewSone.WriteAMessage=You can write a message to this Sone here. Please note that everybody will be able to read this message!
 Page.ViewSone.PostList.Title=Posts by {sone}
 Page.ViewSone.PostList.Text.NoPostYet=This Sone has not yet posted anything.
+Page.ViewSone.Profile.Title=Profile
+Page.ViewSone.Profile.Label.Name=Name
 
 Page.ViewPost.Title=View Post - Sone
 Page.ViewPost.Page.Title=View Post by {sone}
@@ -132,6 +165,21 @@ Page.CreateAlbum.Title=Create Album - Sone
 Page.CreateAlbum.Page.Title=Create Album
 Page.CreateAlbum.Error.NameMissing=You seem to have forgotten to enter a name for your new album.
 
+Page.Trust.Title=Trust Sone - Sone
+
+Page.Distrust.Title=Distrust Sone - Sone
+
+Page.Untrust.Title=Untrust Sone - Sone
+
+Page.MarkAsKnown.Title=Mark as Known - Sone
+
+Page.Bookmark.Title=Bookmark - Sone
+Page.Unbookmark.Title=Remove Bookmark - Sone
+Page.Bookmarks.Title=Bookmarks - Sone
+Page.Bookmarks.Page.Title=Bookmarks
+Page.Bookmarks.Text.NoBookmarks=You don’t have any bookmarks defined right now. You can bookmark posts by clicking the star below the post.
+Page.Bookmarks.Text.PostsNotLoaded=Some of your bookmarked posts have not been shown because they could not be loaded. This can happen if you restarted Sone recently or if the originating Sone has deleted the post. If you are reasonable sure that these posts do not exist anymore, you can {link}unbookmark them{/link}.
+
 Page.NoPermission.Title=Unauthorized Access - Sone
 Page.NoPermission.Page.Title=Unauthorized Access
 Page.NoPermission.Text.NoPermission=You tried to do something that you do not have sufficient authorization for. Please refrain from such actions in the future or we will be forced to take counter-measures!
@@ -143,6 +191,10 @@ Page.WotPluginMissing.Text.LoadPlugin=Please load the Web of Trust plugin in the
 
 Page.Logout.Title=Logout - Sone
 
+Page.Invalid.Title=Invalid Action Performed
+Page.Invalid.Page.Title=Invalid Action Performed
+Page.Invalid.Text=An invalid action was performed, or the action was valid but the parameters were not. Please go back to the {link}index page{/link} and try again. If the error persists you have probably found a bug.
+
 View.CreateSone.Text.WotIdentityRequired=To create a Sone you need an identity from the {link}Web of Trust plugin{/link}.
 View.CreateSone.Select.Default=Select an identity
 View.CreateSone.Text.NoIdentities=You do not have any Web of Trust identities. Please head over to the {link}Web of Trust plugin{/link} and create an identity.
@@ -151,6 +203,7 @@ View.CreateSone.Button.Create=Create Sone
 View.CreateSone.Text.Error.NoIdentity=You have not selected an identity.
 
 View.Sone.Label.LastUpdate=Last update:
+View.Sone.Text.UnknownDate=unknown
 View.Sone.Button.UnlockSone=unlock
 View.Sone.Button.UnlockSone.Tooltip=Allow this Sone to be inserted now
 View.Sone.Button.LockSone=lock
@@ -164,11 +217,20 @@ View.Sone.Status.Downloading=This Sone is currently being downloaded.
 View.Sone.Status.Inserting=This Sone is currently being inserted.
 
 View.Post.UnknownAuthor=(unknown)
+View.Post.Bookmarks.PostIsBookmarked=Post is bookmarked, click to remove from bookmarks
+View.Post.Bookmarks.PostIsNotBookmarked=Post is not bookmarked, click to bookmark
 View.Post.DeleteLink=Delete
 View.Post.SendReply=Post Reply!
 View.Post.Reply.DeleteLink=Delete
 View.Post.LikeLink=Like
 View.Post.UnlikeLink=Unlike
+View.Post.ShowSource=Toggle Parser
+
+View.UpdateStatus.Text.ChooseSenderIdentity=Choose the sender identity
+
+View.Trust.Tooltip.Trust=Trust this person
+View.Trust.Tooltip.Distrust=Assign negative trust to this person
+View.Trust.Tooltip.Untrust=Remove your trust assignment for this person
 
 View.CreateAlbum.Title=Create Album
 View.CreateAlbum.Label.Name=Name:
@@ -182,6 +244,7 @@ WebInterface.DefaultText.LastName=Last name
 WebInterface.DefaultText.BirthDay=Day
 WebInterface.DefaultText.BirthMonth=Month
 WebInterface.DefaultText.BirthYear=Year
+WebInterface.DefaultText.FieldName=Field name
 WebInterface.DefaultText.Option.InsertionDelay=Time to wait after a Sone is modified before insert (in seconds)
 WebInterface.Confirmation.DeletePostButton=Yes, delete!
 WebInterface.Confirmation.DeleteReplyButton=Yes, delete!
@@ -201,10 +264,11 @@ 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:
+Notification.NewPost.Button.MarkRead=Mark as read
 Notification.NewReply.ShortText=New replies have been discovered.
 Notification.NewReply.Text=New replies have been discovered by the following Sones:
 Notification.SoneIsBeingRescued.Text=The following Sones are currently being rescued:
 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}.
+Notification.NewVersion.Text=Version {version} of the Sone plugin was found. Download it from USK@nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI,DuQSUZiI~agF8c-6tjsFFGuZ8eICrzWCILB60nT8KKo,AQACAAE/sone/{edition}​!
index dfd6b9a..8eb8449 100644 (file)
@@ -1,7 +1,7 @@
 /* Sone Main CSS File */
 
 /* first, override some fproxy rules. */
-#sone .post .reply div,#sone .post .time,#sone .post .delete,#sone .post .show-reply-form, input[type=text], textarea {
+#sone div, #sone span, #sone .post .time,#sone .post .delete,#sone .post .show-reply-form, input[type=text], textarea {
        font: inherit;
 }
 
@@ -16,14 +16,30 @@ input[type=text], textarea {
        outline: none;
 }
 
+input[type=text].short {
+       width: 25em;
+}
+
 textarea {
        height: 4em;
 }
 
+#sone button {
+       background-color: #ddd;
+       border-width: 1px;
+       color: #444;
+       padding: 0.5ex 1.5ex;
+}
+
 #sone form {
        margin: 0px;
 }
 
+#sone select {
+       color: #444;
+       padding: 0.5ex 1.5ex;
+}
+
 /* now for the real stuff. */
 
 #sone {
@@ -89,6 +105,10 @@ textarea {
        min-height: 3.5ex;
 }
 
+#sone #notification-area .notification button {
+       margin-left: 1ex;
+}
+
 #sone #notification-area .notification .dismiss {
        float: right;
 }
@@ -97,6 +117,10 @@ textarea {
        margin-left: 1ex;
 }
 
+#sone #notification-area .notification .mark-as-read {
+       float: right;
+}
+
 #sone #plugin-warning {
        border: solid 0.5em red;
        padding: 0.5em;
@@ -136,7 +160,11 @@ textarea {
        padding-left: 1ex;
 }
 
-#sone #update-status label {
+#sone #update-status {
+       margin-bottom: 1em;
+}
+
+#sone #update-status label, #sone #post-message label {
        display: none;
 }
 
@@ -152,6 +180,15 @@ textarea {
        float: right;
 }
 
+#sone #update-status .select-sender, #sone .create-reply .select-sender, #sone #post-message .select-sender {
+       display: none;
+}
+
+#sone #update-status .select-sender button, #sone #post-message .select-sender button {
+       display: inline;
+       float: left;
+}
+
 #sone .nice-name {
        font-weight: bold;
 }
@@ -202,43 +239,86 @@ textarea {
        font-weight: bold;
 }
 
-#sone .post .text {
+#sone .post .text, #sone .post .raw-text {
        display: inline;
        white-space: pre-wrap;
 }
 
+#sone .post .text.hidden, #sone .post .raw-text.hidden {
+       display: none;
+}
+
 #sone .post .status-line {
        margin-top: 0.5ex;
        font-size: 85%;
 }
 
+#sone .separator {
+       font: inherit;
+       color: rgb(28, 131, 191);
+}
+
+#sone .show-source, #sone .show-reply-source {
+       display: inline;
+}
+
+#sone .post .bookmarks {
+       display: inline;
+       color: rgb(28, 131, 191);
+}
+
+#sone .post .bookmark, #sone .post .unbookmark {
+       display: inline;
+       font: inherit;
+       margin: 0px;
+}
+
 #sone .post .time {
        display: inline;
        color: #666;
 }
 
-#sone .post .delete, #sone .post .likes, #sone .post .like, #sone .post .unlike {
+#sone .post .delete, #sone .post .likes, #sone .post .like, #sone .post .unlike, #sone .post .trust, #sone .post .distrust, #sone .post .untrust {
        display: inline;
        font: inherit;
+       margin: 0px;
 }
 
 #sone .post .likes.hidden {
        display: none;
 }
 
-#sone .post .like.hidden, #sone .post .unlike.hidden {
+#sone .post .like.hidden, #sone .post .unlike.hidden, #sone .post .trust.hidden, #sone .post .distrust.hidden, #sone .post .untrust.hidden, #sone .post .bookmark.hidden, #sone .post .unbookmark.hidden {
        display: none;
 }
 
-#sone .post .delete button, #sone .post .like button, #sone .post .unlike button {
+#sone .post .delete button, #sone .post .like button, #sone .post .unlike button, #sone .post .trust button, #sone .post .distrust button, #sone .post .untrust button, #sone .post .bookmark button, #sone .post .unbookmark button {
        border: 0px;
        background: none;
        padding: 0px;
        color: rgb(28, 131, 191);
        font: inherit;
+       margin: 0px;
 }
 
-#sone .post .delete button:hover, #sone .post .like button:hover, #sone .post .unlike button:hover {
+#sone .post .delete button.confirm {
+       color: red;
+       font-weight: bold;
+}
+
+#sone .post .trust button {
+       color: rgb(0, 128, 0);
+}
+
+#sone .post .distrust button {
+       color: rgb(255, 0, 0);
+}
+
+#sone .post .untrust button {
+       color: rgb(64, 64, 64);
+}
+
+#sone .post .delete button:hover, #sone .post .like button:hover, #sone .post .unlike button:hover, #sone .post .trust button:hover, #sone .post .distrust button:hover, #sone .post .untrust button:hover, #sone .post .bookmark button:hover, #sone .post .unbookmark button:hover {
        border: 0px;
        background: none;
        padding: 0px;
@@ -246,10 +326,6 @@ textarea {
        cursor: pointer;
 }
 
-#sone .post .delete:before, #sone .post .likes:before, #sone .post .like:before, #sone .post .unlike:before {
-       content: ' ‧ ';
-}
-
 #sone .post .likes span {
        font: inherit;
        color: green;
@@ -314,10 +390,6 @@ textarea {
        color: rgb(255, 172, 0);
 }
 
-#sone .post .show-reply-form:before {
-       content: ' ‧ ';
-}
-
 #sone .post .create-reply {
        clear: both;
        background-color: #f0f0ff;
@@ -326,12 +398,12 @@ textarea {
 
 #sone .post .create-reply input[type=text] {
        margin-left: 0.5ex;
-       width: 44em;
+       width: 42em;
 }
 
 #sone .post .create-reply textarea {
        margin-left: 0.5ex;
-       width: 44em;
+       width: 42em;
        height: 4em;
 }
 
@@ -339,6 +411,11 @@ textarea {
        float: right;
 }
 
+#sone .create-reply .select-sender button, #sone #post-message .select-sender button {
+       display: inline;
+       float: left;
+}
+
 #sone .sone {
        clear: both;
        background-color: #f0f0ff;
@@ -431,8 +508,41 @@ textarea {
        display: inline;
 }
 
-#sone #create-sone {
+#sone .profile-field, #sone #edit-profile button[type=submit], #sone #delete-profile-field {
+       margin-top: 1em;
+}
 
+#sone .profile-field .name {
+       display: inline;
+       font-weight: bold;
+}
+
+#sone .profile-field .name.hidden {
+       display: none;
+}
+
+#sone .profile-field button.confirm.edit {
+       font-weight: bold;
+       color: #080;
+}
+
+#sone .profile-field button.cancel {
+       font-weight: bold;
+       color: red;
+}
+
+#sone .profile-field .value {
+       margin-left: 2em;
+}
+
+#sone #edit-profile .profile-field .value {
+       margin-left: inherit;
+}
+
+#sone .profile-field .edit-field-name, #sone .profile-field .move-up-field, #sone .profile-field .move-down-field, #sone .profile-field .delete-field-name {
+       float: right;
+       margin-top: -1ex;
+       position: relative;
 }
 
 #sone #tail {
@@ -518,6 +628,6 @@ textarea {
 }
 
 #sone .confirm {
-       font-weight: bold !important;
-       color: red !important;
+       font-weight: bold;
+       color: red;
 }
diff --git a/src/main/resources/static/images/icon-activity.png b/src/main/resources/static/images/icon-activity.png
new file mode 100644 (file)
index 0000000..5803edb
Binary files /dev/null and b/src/main/resources/static/images/icon-activity.png differ
diff --git a/src/main/resources/static/javascript/jquery.url.js b/src/main/resources/static/javascript/jquery.url.js
new file mode 100644 (file)
index 0000000..20d8dfc
--- /dev/null
@@ -0,0 +1,174 @@
+// JQuery URL Parser
+// Written by Mark Perkins, mark@allmarkedup.com
+// License: http://unlicense.org/ (i.e. do what you want with it!)
+
+jQuery.url = function()
+{
+       var segments = {};
+       
+       var parsed = {};
+       
+       /**
+    * Options object. Only the URI and strictMode values can be changed via the setters below.
+    */
+       var options = {
+       
+               url : window.location, // default URI is the page in which the script is running
+               
+               strictMode: false, // 'loose' parsing by default
+       
+               key: ["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"], // keys available to query 
+               
+               q: {
+                       name: "queryKey",
+                       parser: /(?:^|&)([^&=]*)=?([^&]*)/g
+               },
+               
+               parser: {
+                       strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*):?([^:@]*))?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/,  //less intuitive, more accurate to the specs
+                       loose:  /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*):?([^:@]*))?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/ // more intuitive, fails on relative paths and deviates from specs
+               }
+               
+       };
+       
+    /**
+     * Deals with the parsing of the URI according to the regex above.
+        * Written by Steven Levithan - see credits at top.
+     */                
+       var parseUri = function()
+       {
+               str = decodeURI( options.url );
+               
+               var m = options.parser[ options.strictMode ? "strict" : "loose" ].exec( str );
+               var uri = {};
+               var i = 14;
+
+               while ( i-- ) {
+                       uri[ options.key[i] ] = m[i] || "";
+               }
+
+               uri[ options.q.name ] = {};
+               uri[ options.key[12] ].replace( options.q.parser, function ( $0, $1, $2 ) {
+                       if ($1) {
+                               uri[options.q.name][$1] = $2;
+                       }
+               });
+
+               return uri;
+       };
+
+    /**
+     * Returns the value of the passed in key from the parsed URI.
+        * 
+        * @param string key The key whose value is required
+     */                
+       var key = function( key )
+       {
+               if ( jQuery.isEmptyObject(parsed) )
+               {
+                       setUp(); // if the URI has not been parsed yet then do this first...    
+               } 
+               if ( key == "base" )
+               {
+                       if ( parsed.port !== null && parsed.port !== "" )
+                       {
+                               return parsed.protocol+"://"+parsed.host+":"+parsed.port+"/";   
+                       }
+                       else
+                       {
+                               return parsed.protocol+"://"+parsed.host+"/";
+                       }
+               }
+       
+               return ( parsed[key] === "" ) ? null : parsed[key];
+       };
+       
+       /**
+     * Returns the value of the required query string parameter.
+        * 
+        * @param string item The parameter whose value is required
+     */                
+       var param = function( item )
+       {
+               if ( jQuery.isEmptyObject(parsed) )
+               {
+                       setUp(); // if the URI has not been parsed yet then do this first...    
+               }
+               return ( parsed.queryKey[item] === null ) ? null : parsed.queryKey[item];
+       };
+
+    /**
+     * 'Constructor' (not really!) function.
+     *  Called whenever the URI changes to kick off re-parsing of the URI and splitting it up into segments. 
+     */        
+       var setUp = function()
+       {
+               parsed = parseUri();
+               
+               getSegments();  
+       };
+       
+    /**
+     * Splits up the body of the URI into segments (i.e. sections delimited by '/')
+     */
+       var getSegments = function()
+       {
+               var p = parsed.path;
+               segments = []; // clear out segments array
+               segments = parsed.path.length == 1 ? {} : ( p.charAt( p.length - 1 ) == "/" ? p.substring( 1, p.length - 1 ) : path = p.substring( 1 ) ).split("/");
+       };
+       
+       return {
+               
+           /**
+            * Sets the parsing mode - either strict or loose. Set to loose by default.
+            *
+            * @param string mode The mode to set the parser to. Anything apart from a value of 'strict' will set it to loose!
+            */
+               setMode : function( mode )
+               {
+                       options.strictMode = mode == "strict" ? true : false;
+                       return this;
+               },
+               
+               /**
+            * Sets URI to parse if you don't want to to parse the current page's URI.
+                * Calling the function with no value for newUri resets it to the current page's URI.
+            *
+            * @param string newUri The URI to parse.
+            */         
+               setUrl : function( newUri )
+               {
+                       options.url = newUri === undefined ? window.location : newUri;
+                       setUp();
+                       return this;
+               },              
+               
+               /**
+            * Returns the value of the specified URI segment. Segments are numbered from 1 to the number of segments.
+                * For example the URI http://test.com/about/company/ segment(1) would return 'about'.
+                *
+                * If no integer is passed into the function it returns the number of segments in the URI.
+            *
+            * @param int pos The position of the segment to return. Can be empty.
+            */ 
+               segment : function( pos )
+               {
+                       if ( jQuery.isEmptyObject(parsed) )
+                       {
+                               setUp(); // if the URI has not been parsed yet then do this first...    
+                       } 
+                       if ( pos === undefined )
+                       {
+                               return segments.length;
+                       }
+                       return ( segments[pos] === "" || segments[pos] === undefined ) ? null : segments[pos];
+               },
+               
+               attr : key, // provides public access to private 'key' function - see above
+               
+               param : param // provides public access to private 'param' function - see above
+               
+       };
+       
+}();
\ No newline at end of file
index 6bed10f..b6b1257 100644 (file)
@@ -47,6 +47,7 @@ function registerInputTextareaSwap(inputElement, defaultText, inputFieldName, op
                                textarea.show();
                        }
                        $(inputField.get(0).form).submit(function() {
+                               inputField.attr("disabled", "disabled");
                                if (!optional && (textarea.val() == "")) {
                                        return false;
                                }
@@ -64,12 +65,12 @@ function registerInputTextareaSwap(inputElement, defaultText, inputFieldName, op
  *            The element to add a “comment” link to
  */
 function addCommentLink(postId, element, insertAfterThisElement) {
-       if ($(element).find(".show-reply-form").length > 0) {
+       if (($(element).find(".show-reply-form").length > 0) || (getPostElement(element).find(".create-reply").length == 0)) {
                return;
        }
        commentElement = (function(postId) {
+               separator = $("<span> · </span>").addClass("separator");
                var commentElement = $("<div><span>Comment</span></div>").addClass("show-reply-form").click(function() {
-                       markPostAsKnown(getPostElement(this));
                        replyElement = $("#sone .post#" + postId + " .create-reply");
                        replyElement.removeClass("hidden");
                        replyElement.removeClass("light");
@@ -87,6 +88,7 @@ function addCommentLink(postId, element, insertAfterThisElement) {
                return commentElement;
        })(postId);
        $(insertAfterThisElement).after(commentElement.clone(true));
+       $(insertAfterThisElement).after(separator);
 }
 
 var translations = {};
@@ -150,7 +152,13 @@ function updateSoneStatus(soneId, name, status, modified, locked, lastUpdated) {
                toggleClass("modified", modified);
        $("#sone .sone." + filterSoneId(soneId) + " .lock").toggleClass("hidden", locked);
        $("#sone .sone." + filterSoneId(soneId) + " .unlock").toggleClass("hidden", !locked);
-       $("#sone .sone." + filterSoneId(soneId) + " .last-update span.time").text(lastUpdated);
+       if (lastUpdated != null) {
+               $("#sone .sone." + filterSoneId(soneId) + " .last-update span.time").text(lastUpdated);
+       } else {
+               getTranslation("View.Sone.Text.UnknownDate", function(unknown) {
+                       $("#sone .sone." + filterSoneId(soneId) + " .last-update span.time").text(unknown);
+               });
+       }
        $("#sone .sone." + filterSoneId(soneId) + " .profile-link a").text(name);
 }
 
@@ -289,6 +297,17 @@ function getSoneId(element) {
        return getSoneElement(element).find(".id").text();
 }
 
+/**
+ * Returns the element of the post with the given ID.
+ *
+ * @param postId
+ *            The ID of the post
+ * @returns The element of the post
+ */
+function getPost(postId) {
+       return $("#sone .post#" + postId);
+}
+
 function getPostElement(element) {
        return $(element).closest(".post");
 }
@@ -301,6 +320,17 @@ function getPostTime(element) {
        return getPostElement(element).find(".post-time").text();
 }
 
+/**
+ * Returns the author of the post the given element belongs to.
+ *
+ * @param element
+ *            The element whose post to get the author for
+ * @returns The ID of the authoring Sone
+ */
+function getPostAuthor(element) {
+       return getPostElement(element).find(".post-author").text();
+}
+
 function getReplyElement(element) {
        return $(element).closest(".reply");
 }
@@ -313,6 +343,17 @@ function getReplyTime(element) {
        return getReplyElement(element).find(".reply-time").text();
 }
 
+/**
+ * Returns the author of the reply the given element belongs to.
+ *
+ * @param element
+ *            The element whose reply to get the author for
+ * @returns The ID of the authoring Sone
+ */
+function getReplyAuthor(element) {
+       return getReplyElement(element).find(".reply-author").text();
+}
+
 function likePost(postId) {
        $.getJSON("like.ajax", { "type": "post", "post" : postId, "formPassword": getFormPassword() }, function(data, textStatus) {
                if ((data == null) || !data.success) {
@@ -377,6 +418,106 @@ function unlikeReply(replyId) {
        });
 }
 
+/**
+ * Trusts the Sone with the given ID.
+ *
+ * @param soneId
+ *            The ID of the Sone to trust
+ */
+function trustSone(soneId) {
+       $.getJSON("trustSone.ajax", { "formPassword" : getFormPassword(), "sone" : soneId }, function(data, textStatus) {
+               if ((data != null) && data.success) {
+                       updateTrustControls(soneId, data.trustValue);
+               }
+       });
+}
+
+/**
+ * Distrusts the Sone with the given ID, i.e. assigns a negative trust value.
+ *
+ * @param soneId
+ *            The ID of the Sone to distrust
+ */
+function distrustSone(soneId) {
+       $.getJSON("distrustSone.ajax", { "formPassword" : getFormPassword(), "sone" : soneId }, function(data, textStatus) {
+               if ((data != null) && data.success) {
+                       updateTrustControls(soneId, data.trustValue);
+               }
+       });
+}
+
+/**
+ * Untrusts the Sone with the given ID, i.e. removes any trust assignment.
+ *
+ * @param soneId
+ *            The ID of the Sone to untrust
+ */
+function untrustSone(soneId) {
+       $.getJSON("untrustSone.ajax", { "formPassword" : getFormPassword(), "sone" : soneId }, function(data, textStatus) {
+               if ((data != null) && data.success) {
+                       updateTrustControls(soneId, data.trustValue);
+               }
+       });
+}
+
+/**
+ * Updates the trust controls for all posts and replies of the given Sone,
+ * according to the given trust value.
+ *
+ * @param soneId
+ *            The ID of the Sone to update all trust controls for
+ * @param trustValue
+ *            The trust value for the Sone
+ */
+function updateTrustControls(soneId, trustValue) {
+       $("#sone .post").each(function() {
+               if (getPostAuthor(this) == soneId) {
+                       getPostElement(this).find(".post-trust").toggleClass("hidden", trustValue != null);
+                       getPostElement(this).find(".post-distrust").toggleClass("hidden", trustValue != null);
+                       getPostElement(this).find(".post-untrust").toggleClass("hidden", trustValue == null);
+               }
+       });
+       $("#sone .reply").each(function() {
+               if (getReplyAuthor(this) == soneId) {
+                       getReplyElement(this).find(".reply-trust").toggleClass("hidden", trustValue != null);
+                       getReplyElement(this).find(".reply-distrust").toggleClass("hidden", trustValue != null);
+                       getReplyElement(this).find(".reply-untrust").toggleClass("hidden", trustValue == null);
+               }
+       });
+}
+
+/**
+ * Bookmarks the post with the given ID.
+ *
+ * @param postId
+ *            The ID of the post to bookmark
+ */
+function bookmarkPost(postId) {
+       (function(postId) {
+               $.getJSON("bookmark.ajax", {"formPassword": getFormPassword(), "type": "post", "post": postId}, function(data, textStatus) {
+                       if ((data != null) && data.success) {
+                               getPost(postId).find(".bookmark").toggleClass("hidden", true);
+                               getPost(postId).find(".unbookmark").toggleClass("hidden", false);
+                       }
+               });
+       })(postId);
+}
+
+/**
+ * Unbookmarks the post with the given ID.
+ *
+ * @param postId
+ *            The ID of the post to unbookmark
+ */
+function unbookmarkPost(postId) {
+       $.getJSON("unbookmark.ajax", {"formPassword": getFormPassword(), "type": "post", "post": postId}, function(data, textStatus) {
+               if ((data != null) && data.success) {
+                       getPost(postId).find(".bookmark").toggleClass("hidden", false);
+                       getPost(postId).find(".unbookmark").toggleClass("hidden", true);
+               }
+       });
+}
+
 function updateReplyLikes(replyId) {
        $.getJSON("getLikes.ajax", { "type": "reply", "reply": replyId }, function(data, textStatus) {
                if ((data != null) && data.success) {
@@ -392,6 +533,8 @@ function updateReplyLikes(replyId) {
 /**
  * Posts a reply and calls the given callback when the request finishes.
  *
+ * @param sender
+ *            The ID of the sender
  * @param postId
  *            The ID of the post the reply refers to
  * @param text
@@ -400,14 +543,14 @@ function updateReplyLikes(replyId) {
  *            The callback function to call when the request finishes (takes 3
  *            parameters: success, error, replyId)
  */
-function postReply(postId, text, callbackFunction) {
-       $.getJSON("createReply.ajax", { "formPassword" : getFormPassword(), "post" : postId, "text": text }, function(data, textStatus) {
+function postReply(sender, postId, text, callbackFunction) {
+       $.getJSON("createReply.ajax", { "formPassword" : getFormPassword(), "sender": sender, "post" : postId, "text": text }, function(data, textStatus) {
                if (data == null) {
                        /* TODO - show error */
                        return;
                }
                if (data.success) {
-                       callbackFunction(true, null, data.reply);
+                       callbackFunction(true, null, data.reply, data.sone);
                } else {
                        callbackFunction(false, data.error);
                }
@@ -436,6 +579,56 @@ function getReply(replyId, callbackFunction) {
 }
 
 /**
+ * Ajaxifies the given Sone by enhancing all eligible elements with AJAX.
+ *
+ * @param soneElement
+ *            The Sone to ajaxify
+ */
+function ajaxifySone(soneElement) {
+       /*
+        * convert all “follow”, “unfollow”, “lock”, and “unlock” links to something
+        * nicer.
+        */
+       $(".follow", soneElement).submit(function() {
+               var followElement = this;
+               $.getJSON("followSone.ajax", { "sone": getSoneId(this), "formPassword": getFormPassword() }, function() {
+                       $(followElement).addClass("hidden");
+                       $(followElement).parent().find(".unfollow").removeClass("hidden");
+               });
+               return false;
+       });
+       $(".unfollow", soneElement).submit(function() {
+               var unfollowElement = this;
+               $.getJSON("unfollowSone.ajax", { "sone": getSoneId(this), "formPassword": getFormPassword() }, function() {
+                       $(unfollowElement).addClass("hidden");
+                       $(unfollowElement).parent().find(".follow").removeClass("hidden");
+               });
+               return false;
+       });
+       $(".lock", soneElement).submit(function() {
+               var lockElement = this;
+               $.getJSON("lockSone.ajax", { "sone" : getSoneId(this), "formPassword" : getFormPassword() }, function() {
+                       $(lockElement).addClass("hidden");
+                       $(lockElement).parent().find(".unlock").removeClass("hidden");
+               });
+               return false;
+       });
+       $(".unlock", soneElement).submit(function() {
+               var unlockElement = this;
+               $.getJSON("unlockSone.ajax", { "sone" : getSoneId(this), "formPassword" : getFormPassword() }, function() {
+                       $(unlockElement).addClass("hidden");
+                       $(unlockElement).parent().find(".lock").removeClass("hidden");
+               });
+               return false;
+       });
+
+       /* mark Sone as known when clicking it. */
+       $(soneElement).click(function() {
+               markSoneAsKnown(soneElement);
+       });
+}
+
+/**
  * Ajaxifies the given post by enhancing all eligible elements with AJAX.
  *
  * @param postElement
@@ -446,21 +639,24 @@ function ajaxifyPost(postElement) {
                return false;
        });
        $(postElement).find(".create-reply button:submit").click(function() {
-               inputField = $(this.form).find(":input:enabled").get(0);
+               sender = $(this.form).find(":input[name=sender]").val();
+               inputField = $(this.form).find(":input[name=text]:enabled").get(0);
                postId = getPostId(this);
                text = $(inputField).val();
-               (function(postId, text, inputField) {
-                       postReply(postId, text, function(success, error, replyId) {
+               (function(sender, postId, text, inputField) {
+                       postReply(sender, postId, text, function(success, error, replyId, soneId) {
                                if (success) {
                                        $(inputField).val("");
-                                       loadNewReply(replyId);
-                                       markPostAsKnown(getPostElement(inputField));
+                                       loadNewReply(replyId, soneId, postId);
                                        $("#sone .post#" + postId + " .create-reply").addClass("hidden");
+                                       $("#sone .post#" + postId + " .create-reply .sender").hide();
+                                       $("#sone .post#" + postId + " .create-reply .select-sender").show();
+                                       $("#sone .post#" + postId + " .create-reply :input[name=sender]").val(getCurrentSoneId());
                                } else {
                                        alert(error);
                                }
                        });
-               })(postId, text, inputField);
+               })(sender, postId, text, inputField);
                return false;
        });
 
@@ -475,15 +671,46 @@ function ajaxifyPost(postElement) {
        /* convert all “like” buttons to javascript functions. */
        $(postElement).find(".like-post").submit(function() {
                likePost(getPostId(this));
-               markPostAsKnown(getPostElement(this));
                return false;
        });
        $(postElement).find(".unlike-post").submit(function() {
                unlikePost(getPostId(this));
-               markPostAsKnown(getPostElement(this));
                return false;
        });
 
+       /* convert trust control buttons to javascript functions. */
+       $(postElement).find(".post-trust").submit(function() {
+               trustSone(getPostAuthor(this));
+               return false;
+       });
+       $(postElement).find(".post-distrust").submit(function() {
+               distrustSone(getPostAuthor(this));
+               return false;
+       });
+       $(postElement).find(".post-untrust").submit(function() {
+               untrustSone(getPostAuthor(this));
+               return false;
+       });
+
+       /* convert bookmark/unbookmark buttons to javascript functions. */
+       $(postElement).find(".bookmark").submit(function() {
+               bookmarkPost(getPostId(this));
+               return false;
+       });
+       $(postElement).find(".unbookmark").submit(function() {
+               unbookmarkPost(getPostId(this));
+               return false;
+       });
+
+       /* convert “show source” link into javascript function. */
+       $(postElement).find(".show-source").each(function() {
+               $("a", this).click(function() {
+                       $(".post-text.text", getPostElement(this)).toggleClass("hidden");
+                       $(".post-text.raw-text", getPostElement(this)).toggleClass("hidden");
+                       return false;
+               });
+       });
+
        /* add “comment” link. */
        addCommentLink(getPostId(postElement), postElement, $(postElement).find(".post-status-line .time"));
 
@@ -499,8 +726,20 @@ function ajaxifyPost(postElement) {
                });
        });
 
+       /* process sender selection. */
+       $(".select-sender", postElement).css("display", "inline");
+       $(".sender", postElement).hide();
+       $(".select-sender button", postElement).click(function() {
+               $(".sender", postElement).show();
+               $(".select-sender", postElement).hide();
+               return false;
+       });
+
        /* mark everything as known on click. */
-       $(postElement).click(function() {
+       $(postElement).click(function(event) {
+               if ($(event.target).hasClass("click-to-show")) {
+                       return false;
+               }
                markPostAsKnown(this);
        });
 
@@ -517,12 +756,10 @@ function ajaxifyPost(postElement) {
 function ajaxifyReply(replyElement) {
        $(replyElement).find(".like-reply").submit(function() {
                likeReply(getReplyId(this));
-               markPostAsKnown(getPostElement(this));
                return false;
        });
        $(replyElement).find(".unlike-reply").submit(function() {
                unlikeReply(getReplyId(this));
-               markPostAsKnown(getPostElement(this));
                return false;
        });
        (function(replyElement) {
@@ -534,9 +771,27 @@ function ajaxifyReply(replyElement) {
        })(replyElement);
        addCommentLink(getPostId(replyElement), replyElement, $(replyElement).find(".reply-status-line .time"));
 
-       /* mark post and all replies as known on click. */
-       $(replyElement).click(function() {
-               markPostAsKnown(getPostElement(this));
+       /* convert “show source” link into javascript function. */
+       $(replyElement).find(".show-reply-source").each(function() {
+               $("a", this).click(function() {
+                       $(".reply-text.text", getReplyElement(this)).toggleClass("hidden");
+                       $(".reply-text.raw-text", getReplyElement(this)).toggleClass("hidden");
+                       return false;
+               });
+       });
+
+       /* convert trust control buttons to javascript functions. */
+       $(replyElement).find(".reply-trust").submit(function() {
+               trustSone(getReplyAuthor(this));
+               return false;
+       });
+       $(replyElement).find(".reply-distrust").submit(function() {
+               distrustSone(getReplyAuthor(this));
+               return false;
+       });
+       $(replyElement).find(".reply-untrust").submit(function() {
+               untrustSone(getReplyAuthor(this));
+               return false;
        });
 }
 
@@ -547,9 +802,26 @@ function ajaxifyReply(replyElement) {
  *            jQuery object representing the notification.
  */
 function ajaxifyNotification(notification) {
-       notification.find("form.dismiss").submit(function() {
+       notification.find("form").submit(function() {
                return false;
        });
+       notification.find("input[name=returnPage]").val($.url.attr("relative"));
+       if (notification.find(".short-text").length > 0) {
+               notification.find(".short-text").removeClass("hidden");
+               notification.find(".text").addClass("hidden");
+       }
+       notification.find("form.mark-as-read button").click(function() {
+               $.getJSON("markAsKnown.ajax", {"formPassword": getFormPassword(), "type": $(":input[name=type]", this.form).val(), "id": $(":input[name=id]", this.form).val()});
+       });
+       notification.find("a[class^='link-']").each(function() {
+               linkElement = $(this);
+               if (linkElement.is("[href^='viewPost']")) {
+                       id = linkElement.attr("class").substr(5);
+                       if (hasPost(id)) {
+                               linkElement.attr("href", "#post-" + id);
+                       }
+               }
+       });
        notification.find("form.dismiss button").click(function() {
                $.getJSON("dismissNotification.ajax", { "formPassword" : getFormPassword(), "notification" : notification.attr("id") }, function(data, textStatus) {
                        /* dismiss in case of error, too. */
@@ -566,13 +838,18 @@ function getStatus() {
                if ((data != null) && data.success) {
                        /* process Sone information. */
                        $.each(data.sones, function(index, value) {
-                               updateSoneStatus(value.id, value.name, value.status, value.modified, value.locked, value.lastUpdated);
+                               updateSoneStatus(value.id, value.name, value.status, value.modified, value.locked, value.lastUpdatedUnknown ? null : value.lastUpdated);
                        });
                        /* process notifications. */
                        $.each(data.notifications, function(index, value) {
                                oldNotification = $("#sone #notification-area .notification#" + value.id);
                                notification = ajaxifyNotification(createNotification(value.id, value.text, value.dismissable)).hide();
                                if (oldNotification.length != 0) {
+                                       if ((oldNotification.find(".short-text").length > 0) && (notification.find(".short-text").length > 0)) {
+                                               opened = oldNotification.is(":visible") && oldNotification.find(".short-text").hasClass("hidden");
+                                               notification.find(".short-text").toggleClass("hidden", opened);
+                                               notification.find(".text").toggleClass("hidden", !opened);
+                                       }
                                        oldNotification.replaceWith(notification.show());
                                } else {
                                        $("#sone #notification-area").append(notification);
@@ -585,11 +862,11 @@ function getStatus() {
                        });
                        /* process new posts. */
                        $.each(data.newPosts, function(index, value) {
-                               loadNewPost(value);
+                               loadNewPost(value.id, value.sone, value.recipient, value.time);
                        });
                        /* process new replies. */
                        $.each(data.newReplies, function(index, value) {
-                               loadNewReply(value);
+                               loadNewReply(value.id, value.sone, value.post, value.postSone);
                        });
                        /* do it again in 5 seconds. */
                        setTimeout(getStatus, 5000);
@@ -604,6 +881,16 @@ function getStatus() {
 }
 
 /**
+ * Returns the ID of the currently logged in Sone.
+ *
+ * @return The ID of the current Sone, or an empty string if no Sone is logged
+ *         in
+ */
+function getCurrentSoneId() {
+       return $("#currentSoneId").text();
+}
+
+/**
  * Returns the content of the page-id attribute.
  *
  * @returns The page ID
@@ -696,10 +983,20 @@ function hasReply(replyId) {
        return $("#sone .reply#" + replyId).length > 0;
 }
 
-function loadNewPost(postId) {
+function loadNewPost(postId, soneId, recipientId, time) {
        if (hasPost(postId)) {
                return;
        }
+       if (!isIndexPage()) {
+               if (!isViewPostPage() || (getShownPostId() != postId)) {
+                       if (!isViewSonePage() || ((getShownSoneId() != soneId) && (getShownSoneId() != recipientId))) {
+                               return;
+                       }
+               }
+       }
+       if (getPostTime($("#sone .post").last()) > time) {
+               return;
+       }
        $.getJSON("getPost.ajax", { "post" : postId }, function(data, textStatus) {
                if ((data != null) && data.success) {
                        if (hasPost(data.post.id)) {
@@ -718,8 +1015,6 @@ function loadNewPost(postId) {
                        newPost = $(data.post.html).addClass("hidden");
                        if (firstOlderPost != null) {
                                newPost.insertBefore(firstOlderPost);
-                       } else {
-                               $("#sone #posts").append(newPost);
                        }
                        ajaxifyPost(newPost);
                        newPost.slideDown();
@@ -728,10 +1023,13 @@ function loadNewPost(postId) {
        });
 }
 
-function loadNewReply(replyId) {
+function loadNewReply(replyId, soneId, postId, postSoneId) {
        if (hasReply(replyId)) {
                return;
        }
+       if (!hasPost(postId)) {
+               return;
+       }
        $.getJSON("getReply.ajax", { "reply": replyId }, function(data, textStatus) {
                /* find post. */
                if ((data != null) && data.success) {
@@ -765,14 +1063,28 @@ function loadNewReply(replyId) {
        });
 }
 
+/**
+ * Marks the given Sone as known if it is still new.
+ *
+ * @param soneElement
+ *            The Sone to mark as known
+ */
+function markSoneAsKnown(soneElement) {
+       if ($(".new", soneElement).length > 0) {
+               $.getJSON("maskAsKnown.ajax", {"formPassword": getFormPassword(), "type": "sone", "id": getSoneId(soneElement)}, function(data, textStatus) {
+                       $(soneElement).removeClass("new");
+               });
+       }
+}
+
 function markPostAsKnown(postElements) {
        $(postElements).each(function() {
                postElement = this;
                if ($(postElement).hasClass("new")) {
                        (function(postElement) {
-                               $.getJSON("markPostAsKnown.ajax", {"formPassword": getFormPassword(), "post": getPostId(postElement)}, function(data, textStatus) {
-                                       $(postElement).removeClass("new");
-                               });
+                               $(postElement).removeClass("new");
+                               $(".click-to-show", postElement).removeClass("new");
+                               $.getJSON("markAsKnown.ajax", {"formPassword": getFormPassword(), "type": "post", "id": getPostId(postElement)});
                        })(postElement);
                }
        });
@@ -784,9 +1096,8 @@ function markReplyAsKnown(replyElements) {
                replyElement = this;
                if ($(replyElement).hasClass("new")) {
                        (function(replyElement) {
-                               $.getJSON("markReplyAsKnown.ajax", {"formPassword": getFormPassword(), "reply": getReplyId(replyElement)}, function(data, textStatus) {
-                                       $(replyElement).removeClass("new");
-                               });
+                               $(replyElement).removeClass("new");
+                               $.getJSON("markAsKnown.ajax", {"formPassword": getFormPassword(), "type": "reply", "id": getReplyId(replyElement)});
                        })(replyElement);
                }
        });
@@ -795,7 +1106,7 @@ function markReplyAsKnown(replyElements) {
 function resetActivity() {
        title = document.title;
        if (title.indexOf('(') == 0) {
-               document.title = title.substr(title.indexOf(' ') + 1);
+               setTitle(title.substr(title.indexOf(' ') + 1));
        }
 }
 
@@ -803,12 +1114,65 @@ function setActivity() {
        if (!focus) {
                title = document.title;
                if (title.indexOf('(') != 0) {
-                       document.title = "(!) " + title;
+                       setTitle("(!) " + title);
+               }
+               if (!iconBlinking) {
+                       setTimeout(toggleIcon, 1500);
+                       iconBlinking = true;
+               }
+       }
+}
+
+/**
+ * Sets the window title after a small delay to prevent race-condition issues.
+ *
+ * @param title
+ *            The title to set
+ */
+function setTitle(title) {
+       setTimeout(function() {
+               document.title = title;
+       }, 50);
+}
+
+/** Whether the icon is currently showing activity. */
+var iconActive = false;
+
+/** Whether the icon is currently supposed to blink. */
+var iconBlinking = false;
+
+/**
+ * Toggles the icon. If the window has gained focus and the icon is still
+ * showing the activity state, it is returned to normal.
+ */
+function toggleIcon() {
+       if (focus) {
+               if (iconActive) {
+                       changeIcon("images/icon.png");
+                       iconActive = false;
                }
+               iconBlinking = false;
+       } else {
+               iconActive = !iconActive;
+               console.log("showing icon: " + iconActive);
+               changeIcon(iconActive ? "images/icon-activity.png" : "images/icon.png");
+               setTimeout(toggleIcon, 1500);
        }
 }
 
 /**
+ * Changes the icon of the page.
+ *
+ * @param iconUrl
+ *            The new URL of the icon
+ */
+function changeIcon(iconUrl) {
+       $("link[rel=icon]").remove();
+       $("head").append($("<link>").attr("rel", "icon").attr("type", "image/png").attr("href", iconUrl));
+       $("iframe[id=icon-update]")[0].src += "";
+}
+
+/**
  * Creates a new notification.
  *
  * @param id
@@ -837,8 +1201,82 @@ function createNotification(id, text, dismissable) {
  *            The ID of the notification
  */
 function showNotificationDetails(notificationId) {
-       $("#sone .notification#" + notificationId + " .text").show();
-       $("#sone .notification#" + notificationId + " .short-text").hide();
+       $("#sone .notification#" + notificationId + " .text").removeClass("hidden");
+       $("#sone .notification#" + notificationId + " .short-text").addClass("hidden");
+}
+
+/**
+ * Deletes the field with the given ID from the profile.
+ *
+ * @param fieldId
+ *            The ID of the field to delete
+ */
+function deleteProfileField(fieldId) {
+       $.getJSON("deleteProfileField.ajax", {"formPassword": getFormPassword(), "field": fieldId}, function(data, textStatus) {
+               if (data && data.success) {
+                       $("#sone .profile-field#" + data.field.id).slideUp();
+               }
+       });
+}
+
+/**
+ * Renames a profile field.
+ *
+ * @param fieldId
+ *            The ID of the field to rename
+ * @param newName
+ *            The new name of the field
+ * @param successFunction
+ *            Called when the renaming was successful
+ */
+function editProfileField(fieldId, newName, successFunction) {
+       $.getJSON("editProfileField.ajax", {"formPassword": getFormPassword(), "field": fieldId, "name": newName}, function(data, textStatus) {
+               if (data && data.success) {
+                       successFunction();
+               }
+       });
+}
+
+/**
+ * Moves the profile field with the given ID one slot in the given direction.
+ *
+ * @param fieldId
+ *            The ID of the field to move
+ * @param direction
+ *            The direction to move in (“up” or “down”)
+ * @param successFunction
+ *            Function to call on success
+ */
+function moveProfileField(fieldId, direction, successFunction) {
+       $.getJSON("moveProfileField.ajax", {"formPassword": getFormPassword(), "field": fieldId, "direction": direction}, function(data, textStatus) {
+               if (data && data.success) {
+                       successFunction();
+               }
+       });
+}
+
+/**
+ * Moves the profile field with the given ID up one slot.
+ *
+ * @param fieldId
+ *            The ID of the field to move
+ * @param successFunction
+ *            Function to call on success
+ */
+function moveProfileFieldUp(fieldId, successFunction) {
+       moveProfileField(fieldId, "up", successFunction);
+}
+
+/**
+ * Moves the profile field with the given ID down one slot.
+ *
+ * @param fieldId
+ *            The ID of the field to move
+ * @param successFunction
+ *            Function to call on success
+ */
+function moveProfileFieldDown(fieldId, successFunction) {
+       moveProfileField(fieldId, "down", successFunction);
 }
 
 //
@@ -852,17 +1290,28 @@ $(document).ready(function() {
        /* this initializes the status update input field. */
        getTranslation("WebInterface.DefaultText.StatusUpdate", function(defaultText) {
                registerInputTextareaSwap("#sone #update-status .status-input", defaultText, "text", false, false);
+               $("#sone #update-status .select-sender").css("display", "inline");
+               $("#sone #update-status .sender").hide();
+               $("#sone #update-status .select-sender button").click(function() {
+                       $("#sone #update-status .sender").show();
+                       $("#sone #update-status .select-sender").hide();
+                       return false;
+               });
                $("#sone #update-status").submit(function() {
                        if ($(this).find(":input.default:enabled").length > 0) {
                                return false;
                        }
-                       text = $(this).find(":input:enabled").val();
-                       $.getJSON("createPost.ajax", { "formPassword": getFormPassword(), "text": text }, function(data, textStatus) {
+                       sender = $(this).find(":input[name=sender]").val();
+                       text = $(this).find(":input[name=text]:enabled").val();
+                       $.getJSON("createPost.ajax", { "formPassword": getFormPassword(), "sender": sender, "text": text }, function(data, textStatus) {
                                if ((data != null) && data.success) {
-                                       loadNewPost(data.postId);
+                                       loadNewPost(data.postId, data.sone, data.recipient);
                                }
                        });
-                       $(this).find(":input:enabled").val("").blur();
+                       $(this).find(":input[name=sender]").val(getCurrentSoneId());
+                       $(this).find(":input[name=text]:enabled").val("").blur();
+                       $(this).find(".sender").hide();
+                       $(this).find(".select-sender").show();
                        return false;
                });
        });
@@ -870,14 +1319,25 @@ $(document).ready(function() {
        /* ajaxify input field on “view Sone” page. */
        getTranslation("WebInterface.DefaultText.Message", function(defaultText) {
                registerInputTextareaSwap("#sone #post-message input[name=text]", defaultText, "text", false, false);
+               $("#sone #post-message .select-sender").css("display", "inline");
+               $("#sone #post-message .sender").hide();
+               $("#sone #post-message .select-sender button").click(function() {
+                       $("#sone #post-message .sender").show();
+                       $("#sone #post-message .select-sender").hide();
+                       return false;
+               });
                $("#sone #post-message").submit(function() {
-                       text = $(this).find(":input:enabled").val();
-                       $.getJSON("createPost.ajax", { "formPassword": getFormPassword(), "recipient": getShownSoneId(), "text": text }, function(data, textStatus) {
+                       sender = $(this).find(":input[name=sender]").val();
+                       text = $(this).find(":input[name=text]:enabled").val();
+                       $.getJSON("createPost.ajax", { "formPassword": getFormPassword(), "recipient": getShownSoneId(), "sender": sender, "text": text }, function(data, textStatus) {
                                if ((data != null) && data.success) {
-                                       loadNewPost(data.postId);
+                                       loadNewPost(data.postId, getCurrentSoneId());
                                }
                        });
-                       $(this).find(":input:enabled").val("").blur();
+                       $(this).find(":input[name=sender]").val(getCurrentSoneId());
+                       $(this).find(":input[name=text]:enabled").val("").blur();
+                       $(this).find(".sender").hide();
+                       $(this).find(".select-sender").show();
                        return false;
                });
        });
@@ -929,41 +1389,8 @@ $(document).ready(function() {
                });
        }
 
-       /*
-        * convert all “follow”, “unfollow”, “lock”, and “unlock” links to something
-        * nicer.
-        */
-       $("#sone .follow").submit(function() {
-               var followElement = this;
-               $.getJSON("followSone.ajax", { "sone": getSoneId(this), "formPassword": getFormPassword() }, function() {
-                       $(followElement).addClass("hidden");
-                       $(followElement).parent().find(".unfollow").removeClass("hidden");
-               });
-               return false;
-       });
-       $("#sone .unfollow").submit(function() {
-               var unfollowElement = this;
-               $.getJSON("unfollowSone.ajax", { "sone": getSoneId(this), "formPassword": getFormPassword() }, function() {
-                       $(unfollowElement).addClass("hidden");
-                       $(unfollowElement).parent().find(".follow").removeClass("hidden");
-               });
-               return false;
-       });
-       $("#sone .lock").submit(function() {
-               var lockElement = this;
-               $.getJSON("lockSone.ajax", { "sone" : getSoneId(this), "formPassword" : getFormPassword() }, function() {
-                       $(lockElement).addClass("hidden");
-                       $(lockElement).parent().find(".unlock").removeClass("hidden");
-               });
-               return false;
-       });
-       $("#sone .unlock").submit(function() {
-               var unlockElement = this;
-               $.getJSON("unlockSone.ajax", { "sone" : getSoneId(this), "formPassword" : getFormPassword() }, function() {
-                       $(unlockElement).addClass("hidden");
-                       $(unlockElement).parent().find(".lock").removeClass("hidden");
-               });
-               return false;
+       $("#sone .sone").each(function() {
+               ajaxifySone($(this));
        });
 
        /* process all existing notifications, ajaxify dismiss buttons. */
diff --git a/src/main/resources/templates/bookmarks.html b/src/main/resources/templates/bookmarks.html
new file mode 100644 (file)
index 0000000..8768b5a
--- /dev/null
@@ -0,0 +1,20 @@
+<%include include/head.html>
+
+       <div class="page-id hidden">bookmarks</div>
+
+       <h1><%= Page.Bookmarks.Page.Title|l10n|html></h1>
+
+       <div id="posts">
+               <%include include/pagination.html>
+               <%foreach posts post>
+                       <%include include/viewPost.html>
+               <%/foreach>
+               <%include include/pagination.html>
+               <%if postsNotLoaded>
+                       <p><%= Page.Bookmarks.Text.PostsNotLoaded|l10n|html|replace needle='{link}' replacement='<a href="unbookmark.html?post=allNotLoaded">'|replace needle='{/link}' replacement='</a>'></p>
+               <%elseif posts.empty>
+                       <p><%= Page.Bookmarks.Text.NoBookmarks|l10n|html></p>
+               <%/if>
+       </div>
+
+<%include include/tail.html>
diff --git a/src/main/resources/templates/deleteProfileField.html b/src/main/resources/templates/deleteProfileField.html
new file mode 100644 (file)
index 0000000..006e1ff
--- /dev/null
@@ -0,0 +1,19 @@
+<%include include/head.html>
+
+       <h1><%= Page.DeleteProfileField.Page.Title|l10n|html></h1>
+
+       <p><%= Page.DeleteProfileField.Text|l10n|html></p>
+
+       <div class="profile-field">
+               <div class="name"><% field.name|html></div>
+               <div class="value"><% field.value|html></div>
+       </div>
+
+       <form id="delete-profile-field" method="post">
+               <input type="hidden" name="formPassword" value="<% formPassword|html>" />
+               <input type="hidden" name="field" value="<% field.id|html>" />
+               <button type="submit" name="confirm" value="true"><%= Page.DeleteProfileField.Button.Yes|l10n|html></button>
+               <button type="submit" name="cancel" value="true"><%= Page.DeleteProfileField.Button.No|l10n|html></button>
+       </form>
+
+<%include include/tail.html>
index d1d5943..ecde2f7 100644 (file)
@@ -1,7 +1,14 @@
 <%include include/head.html>
 
        <script language="javascript">
-               $(document).ready(function() {
+               function recheckMoveButtons() {
+                       $("#sone .profile-field").each(function() {
+                               $(".move-up-field", this).toggleClass("hidden", $(this).prev(".profile-field").length == 0);
+                               $(".move-down-field", this).toggleClass("hidden", $(this).next(".profile-field").length == 0);
+                       });
+               }
+
+               $(function() {
                        getTranslation("WebInterface.DefaultText.FirstName", function(firstNameDefaultText) {
                                registerInputTextareaSwap("#sone #edit-profile input[name=first-name]", firstNameDefaultText, "first-name", true, true);
                        });
                        getTranslation("WebInterface.DefaultText.BirthYear", function(birthYearDefaultText) {
                                registerInputTextareaSwap("#sone #edit-profile input[name=birth-year]", birthYearDefaultText, "birth-year", true, true);
                        });
+                       getTranslation("WebInterface.DefaultText.FieldName", function(fieldNameDefaultText) {
+                               registerInputTextareaSwap("#sone #add-profile-field input[name=field-name]", fieldNameDefaultText, "field-name", true, true);
+                       });
+
+                       <%foreach fields field>
+                               registerInputTextareaSwap("#sone #edit-profile input[name=field-<% loop.count>]", <% field.key|js>, "field-<% loop.count>", true, true);
+                       <%/foreach>
 
                        /* hide all the labels. */
-                       $("#sone #edit-profile label").hide();
+                       $("#sone #edit-profile label, #sone #add-profile-field label").hide();
+
+                       /* ajaxify the delete buttons. */
+                       getTranslation("Page.EditProfile.Fields.Button.ReallyDelete", function(reallyDeleteText) {
+                               $("#sone #edit-profile .delete-field-name button").each(function() {
+                                       confirmButton = $(this).clone().addClass("hidden").addClass("confirm").text(reallyDeleteText).insertAfter(this);
+                                       (function(deleteButton, confirmButton) {
+                                               deleteButton.click(function() {
+                                                       deleteButton.fadeOut("slow", function() {
+                                                               confirmButton.fadeIn("slow");
+                                                               $(document).one("click", function() {
+                                                                       if (this != confirmButton.get(0)) {
+                                                                               confirmButton.fadeOut("slow", function() {
+                                                                                       deleteButton.fadeIn("slow");
+                                                                               });
+                                                                       }
+                                                                       return false;
+                                                               });
+                                                       });
+                                                       return false;
+                                               });
+                                               confirmButton.click(function() {
+                                                       confirmButton.fadeOut("slow");
+                                                       buttonName = confirmButton.attr("name");
+                                                       fieldId = buttonName.substring("delete-field-".length);
+                                                       deleteProfileField(fieldId);
+                                                       recheckMoveButtons();
+                                                       return false;
+                                               });
+                                       })($(this), confirmButton);
+                               });
+                       });
+
+                       /* ajaxify the edit button. */
+                       $("#sone #edit-profile .edit-field-name button").each(function() {
+                               profileField = $(this).parents(".profile-field");
+                               fieldNameElement = profileField.find(".name");
+                               inputField = $("input[type=text].short", profileField);
+                               confirmButton = $("button.confirm", profileField);
+                               cancelButton = $("button.cancel", profileField);
+                               (function(editButton, inputField, confirmButton, cancelButton, fieldNameElement) {
+                                       cleanUp = function(editButton, inputField, confirmButton, cancelButton, fieldNameElement) {
+                                               editButton.removeAttr("disabled");
+                                               inputField.addClass("hidden");
+                                               confirmButton.addClass("hidden");
+                                               cancelButton.addClass("hidden");
+                                               fieldNameElement.removeClass("hidden");
+                                       };
+                                       confirmButton.click(function() {
+                                               inputField.attr("disabled", "disabled");
+                                               confirmButton.attr("disabled", "disabled");
+                                               cancelButton.attr("disabled", "disabled");
+                                               editProfileField(confirmButton.parents(".profile-field").attr("id"), inputField.val(), function() {
+                                                       fieldNameElement.text(inputField.val());
+                                                       cleanUp(editButton, inputField, confirmButton, cancelButton, fieldNameElement);
+                                               });
+                                               return false;
+                                       });
+                                       cancelButton.click(function() {
+                                               cleanUp(editButton, inputField, confirmButton, cancelButton, fieldNameElement);
+                                               return false;
+                                       });
+                                       inputField.keypress(function(event) {
+                                               if (event.which == 13) {
+                                                       confirmButton.click();
+                                                       return false;
+                                               } else if (event.which == 27) {
+                                                       cancelButton.click();
+                                                       return false;
+                                               }
+                                       });
+                                       editButton.click(function() {
+                                               editButton.attr("disabled", "disabled");
+                                               fieldNameElement.addClass("hidden");
+                                               inputField.removeAttr("disabled").val(fieldNameElement.text()).removeClass("hidden").focus().select();
+                                               confirmButton.removeAttr("disabled").removeClass("hidden");
+                                               cancelButton.removeAttr("disabled").removeClass("hidden");
+                                               return false;
+                                       });
+                               })($(this), inputField, confirmButton, cancelButton, fieldNameElement);
+                       });
+
+                       /* ajaxify “move up” and “move down” buttons. */
+                       $("#sone .profile-field .move-down-field button").click(function() {
+                               profileField = $(this).parents(".profile-field");
+                               moveProfileFieldDown(profileField.attr("id"), function() {
+                                       next = profileField.next();
+                                       current = profileField.insertAfter(next);
+                                       recheckMoveButtons();
+                               });
+                               return false;
+                       });
+                       $("#sone .profile-field .move-up-field button").click(function() {
+                               profileField = $(this).parents(".profile-field");
+                               moveProfileFieldUp(profileField.attr("id"), function() {
+                                       previous = profileField.prev();
+                                       current = profileField.insertBefore(previous);
+                                       recheckMoveButtons();
+                               });
+                               return false;
+                       });
                });
        </script>
 
        <p><%= Page.EditProfile.Page.Description|l10n|html></p>
        <p><%= Page.EditProfile.Page.Hint.Optionality|l10n|html></p>
 
-       <%if changed>
-               <p><%= Page.EditProfile.Page.Status.Changed|l10n|html></p>
-       <%/if>
-
        <form id="edit-profile" method="post">
                <input type="hidden" name="formPassword" value="<% formPassword|html>" />
 
                </div>
 
                <div>
-                       <button type="submit"><%= Page.EditProfile.Button.Save|l10n|html></button>
+                       <button type="submit" name="save-profile" value="true"><%= Page.EditProfile.Button.Save|l10n|html></button>
+               </div>
+
+               <h1><%= Page.EditProfile.Fields.Title|l10n|html></h1>
+
+               <p><%= Page.EditProfile.Fields.Description|l10n|html></p>
+
+               <%foreach fields field fieldLoop>
+                       <div class="profile-field" id="<% field.id|html>">
+                               <div class="name"><% field.name|html></div>
+                               <input class="short hidden" type="text"><button class="edit confirm hidden" type="button">✔</button><button class="cancel hidden" type="button">✘</button>
+                               <div class="edit-field-name"><button type="submit" name="edit-field-<% field.id|html>" value="true"><%= Page.EditProfile.Fields.Button.Edit|l10n|html></button></div>
+                               <div class="delete-field-name"><button type="submit" name="delete-field-<% field.id|html>" value="true"><%= Page.EditProfile.Fields.Button.Delete|l10n|html></button></div>
+                               <div class="<%if fieldLoop.last>hidden <%/if>move-down-field"><button type="submit" name="move-down-field-<% field.id|html>" value="true"><%= Page.EditProfile.Fields.Button.MoveDown|l10n|html></button></div>
+                               <div class="<%if fieldLoop.first>hidden <%/if>move-up-field"><button type="submit" name="move-up-field-<% field.id|html>" value="true"><%= Page.EditProfile.Fields.Button.MoveUp|l10n|html></button></div>
+                               <div class="value"><input type="text" name="field-<% field.id|html>" value="<% field.value|html>" /></div>
+                       </div>
+
+                       <%if fieldLoop.last>
+                               <div>
+                                       <button type="submit" name="save-profile" value="true"><%= Page.EditProfile.Button.Save|l10n|html></button>
+                               </div>
+                       <%/if>
+               <%/foreach>
+
+       </form>
+
+       <form id="add-profile-field" method="post">
+               <input type="hidden" name="formPassword" value="<% formPassword|html>" />
+
+               <a name="profile-fields"></a>
+               <h2><%= Page.EditProfile.Fields.AddField.Title|l10n|html></h2>
+
+               <%if duplicateFieldName>
+                       <p><%= Page.EditProfile.Error.DuplicateFieldName|l10n|replace needle="{fieldName}" replacementKey="fieldName"|html></p>
+               <%/if>
+
+               <div id="new-field">
+                       <label for="new-field"><%= Page.EditProfile.Fields.AddField.Label.Name|l10n|html></label>
+                       <input type="text" name="field-name" value="" />
+                       <button type="submit" name="add-field" value="true"><%= Page.EditProfile.Fields.AddField.Button.AddField|l10n|html></button>
                </div>
 
        </form>
diff --git a/src/main/resources/templates/editProfileField.html b/src/main/resources/templates/editProfileField.html
new file mode 100644 (file)
index 0000000..6030f71
--- /dev/null
@@ -0,0 +1,24 @@
+<%include include/head.html>
+
+       <h1><%= Page.EditProfileField.Page.Title|l10n|html></h1>
+
+       <p><%= Page.EditProfileField.Text|l10n|html></p>
+
+       <%if duplicateFieldName>
+               <p><%= Page.EditProfileField.Error.DuplicateFieldName|l10n|html></p>
+       <%/if>
+
+       <form method="post">
+               <input type="hidden" name="formPassword" value="<% formPassword|html>" />
+               <input type="hidden" name="field" value="<% field.id|html>" />
+               <div>
+                       <input type="text" name="name" value="<% field.name|html>" />
+                       <button type="submit" name="save" value="true"><%= Page.EditProfileField.Button.Save|l10n|html></button>
+               </div>
+               <p>
+                       <button type="reset"><%= Page.EditProfileField.Button.Reset|l10n|html></button>
+                       <button type="submit" name="cancel" value="true"><%= Page.EditProfileField.Button.Cancel|l10n|html></button>
+               </p>
+       </form>
+
+<%include include/tail.html>
index 1d8e284..a9fbb49 100644 (file)
@@ -1,8 +1,10 @@
 <div id="sone" class="<%ifnull ! currentSone>online<%else>offline<%/if>">
 
        <div id="formPassword"><% formPassword|html></div>
+       <div id="currentSoneId" class="hidden"><% currentSone.id|html></div>
 
        <script src="javascript/jquery-1.4.2.js" language="javascript"></script>
+       <script src="javascript/jquery.url.js" language="javascript"></script>
        <script src="javascript/sone.js" language="javascript"></script>
 
        <div id="main">
@@ -26,7 +28,7 @@
                                                        <button type="submit"><%= Notification.Button.Dismiss|l10n|html></button>
                                                </form>
                                        <%/if>
-                                       <% notification>
+                                       <%include notification>
                                </div>
                        <%/foreach>
                </div>
@@ -43,7 +45,6 @@
                                <div id="home-sone">
                                        <% currentSone|store key=sone>
                                        <%include include/viewSone.html>
-                                       <%include include/updateStatus.html>
                                </div>
                        <%/if>
                </div>
index dd2e25e..ec2b587 100644 (file)
@@ -16,4 +16,6 @@
                </div>
        </div>
 
+       <iframe id="icon-update" class="hidden" src="about:blank"></iframe>
+
 </div>
index fba951d..43d4d70 100644 (file)
@@ -1,7 +1,15 @@
 <form id="update-status" action="createPost.html" method="post">
        <input type="hidden" name="formPassword" value="<% formPassword|html>" />
        <input type="hidden" name="returnPage" value="<% request.uri|html>" />
-       <label for="text"><%= Page.Index.Label.Text|l10n|html></label>
+       <label for="sender"><%= Page.Index.Label.Sender|l10n|html></label>
+       <div class="sender">
+               <select name="sender" title="<%= View.UpdateStatus.Text.ChooseSenderIdentity|l10n|html>">
+                       <%foreach localSones localSone>
+                               <option value="<% localSone.id|html>"<%if localSone.current> selected="selected"<%/if>><% localSone.niceName|html></option>
+                       <%/foreach>
+               </select>
+       </div>
+       <div class="select-sender"><button type="button" title="<%= View.UpdateStatus.Text.ChooseSenderIdentity|l10n|html>">+</button></div><label for="text"><%= Page.Index.Label.Text|l10n|html></label>
        <input type="text" class="status-input" name="text" value="" />
        <button type="submit"><%= Page.Index.Button.Post|l10n|html></button>
 </form>
index cd6cc5c..3023566 100644 (file)
@@ -1,6 +1,7 @@
-<a name="post-<% post.id|html>"></a>
 <div id="<% post.id|html>" class="post<%if loop.last> last<%/if><%if post.new> new<%/if>">
+       <a name="post-<% post.id|html>"></a>
        <div class="post-time hidden"><% post.time|html></div>
+       <div class="post-author hidden"><% post.sone.id|html></div>
        <div class="avatar">
                <img src="/WoT/GetIdenticon?identity=<% post.sone.id|html>&amp;width=48&height=48" width="48" height="48" alt="Avatar Image" />
        </div>
                                        <div class="recipient profile-link"><a href="viewSone.html?sone=<% post.recipient.id|html>"><% post.recipient.niceName|html></a></div>
                                <%/if>
                        <%/if>
-                       <div class="text"><% post.text></div>
+                       <div class="post-text raw-text<%if !raw> hidden<%/if>"><% post.text|html></div>
+                       <div class="post-text text<%if raw> hidden<%/if>"><% post.text|parse sone=post.sone></div>
                </div>
                <div class="post-status-line status-line">
+                       <div class="bookmarks">
+                               <form class="unbookmark<%if !post.bookmarked> hidden<%/if>" action="unbookmark.html" method="post">
+                                       <input type="hidden" name="formPassword" value="<% formPassword|html>" />
+                                       <input type="hidden" name="returnPage" value="<% request.uri|html>" />
+                                       <input type="hidden" name="post" value="<% post.id|html>" />
+                                       <button type="submit" title="<%= View.Post.Bookmarks.PostIsBookmarked|l10n|html>">★</button>
+                               </form>
+                               <form class="bookmark<%if post.bookmarked> hidden<%/if>" action="bookmark.html" method="post">
+                                       <input type="hidden" name="formPassword" value="<% formPassword|html>" />
+                                       <input type="hidden" name="returnPage" value="<% request.uri|html>" />
+                                       <input type="hidden" name="post" value="<% post.id|html>" />
+                                       <button type="submit" title="<%= View.Post.Bookmarks.PostIsNotBookmarked|l10n|html>">☆</button>
+                               </form>
+                       </div>
+                       <span class='separator'>·</span>
                        <div class="time"><a href="viewPost.html?post=<% post.id|html>"><% post.time|date format="MMM d, yyyy, HH:mm:ss"></a></div>
-                       <div class="likes<%if post.likes.size|match value=0> hidden<%/if>"><span title="<% post.likes.soneNames|html>">↑<span class="like-count"><% post.likes.size></span></span></div>
+                       <span class='separator'>·</span>
+                       <div class="show-source"><a href="viewPost.html?post=<% post.id|html>&amp;raw=<%if raw>false<%else>true<%/if>"><%= View.Post.ShowSource|l10n|html></a></div>
+                       <div class="likes<%if post.likes.size|match value=0> hidden<%/if>">
+                               <span class='separator'>·</span>
+                               <span title="<% post.likes.soneNames|html>">↑<span class="like-count"><% post.likes.size></span></span>
+                       </div>
                        <%ifnull ! currentSone>
+                               <span class='separator'>·</span>
                                <form class="like like-post<%if post.liked> hidden<%/if>" action="like.html" method="post">
                                        <input type="hidden" name="formPassword" value="<% formPassword|html>" />
                                        <input type="hidden" name="returnPage" value="<% request.uri|html>" />
                                        <input type="hidden" name="post" value="<% post.id|html>" />
                                        <button type="submit" value="1"><%= View.Post.UnlikeLink|l10n|html></button>
                                </form>
+                               <%if !post.sone.current>
+                                       <span class='separator'>·</span>
+                                       <form class="trust post-trust<%if post.sone.trust.assigned> hidden<%/if>" action="trust.html" method="post">
+                                               <input type="hidden" name="formPassword" value="<% formPassword|html>" />
+                                               <input type="hidden" name="returnPage" value="<% request.uri|html>" />
+                                               <input type="hidden" name="sone" value="<% post.sone.id|html>" />
+                                               <button type="submit" title="<%= View.Trust.Tooltip.Trust|l10n|html>">✓</button>
+                                       </form>
+                                       <form class="distrust post-distrust<%if post.sone.trust.assigned> hidden<%/if>" action="distrust.html" method="post">
+                                               <input type="hidden" name="formPassword" value="<% formPassword|html>" />
+                                               <input type="hidden" name="returnPage" value="<% request.uri|html>" />
+                                               <input type="hidden" name="sone" value="<% post.sone.id|html>" />
+                                               <button type="submit" title="<%= View.Trust.Tooltip.Distrust|l10n|html>">✗</button>
+                                       </form>
+                                       <form class="untrust post-untrust<%if !post.sone.trust.assigned> hidden<%/if>" action="untrust.html" method="post">
+                                               <input type="hidden" name="formPassword" value="<% formPassword|html>" />
+                                               <input type="hidden" name="returnPage" value="<% request.uri|html>" />
+                                               <input type="hidden" name="sone" value="<% post.sone.id|html>" />
+                                               <button type="submit" title="<%= View.Trust.Tooltip.Untrust|l10n|html>">↶</button>
+                                       </form>
+                               <%/if>
                        <%/if>
-                       <%if post.sone.current>
+                       <%if post.sone.local>
+                               <span class='separator'>·</span>
                                <form class="delete delete-post" action="deletePost.html" method="post">
                                        <input type="hidden" name="formPassword" value="<% formPassword|html>" />
                                        <input type="hidden" name="returnPage" value="<% request.uri|html>" />
                                                <input type="hidden" name="formPassword" value="<% formPassword|html>" />
                                                <input type="hidden" name="returnPage" value="<% request.uri|html>" />
                                                <input type="hidden" name="post" value="<% post.id|html>" />
+                                               <div class="sender">
+                                                       <select name="sender" title="<%= View.UpdateStatus.Text.ChooseSenderIdentity|l10n|html>">
+                                                               <%foreach localSones localSone|sort>
+                                                                       <option value="<% localSone.id|html>"<%if localSone.current> selected="selected"<%/if>><% localSone.niceName|html></option>
+                                                               <%/foreach>
+                                                       </select>
+                                               </div>
+                                               <div class="select-sender"><button type="button" title="<%= View.UpdateStatus.Text.ChooseSenderIdentity|l10n|html>">+</button></div>
                                                <input type="text" class="reply-input" name="text" value="" />
                                                <button type="submit"><%= View.Post.SendReply|l10n|html></button>
                                        </form>
index f65a201..cdb837c 100644 (file)
@@ -1,18 +1,26 @@
-<a name="reply-<% reply.id|html>"></a>
 <div id="<% reply.id|html>" class="reply<%if reply.new> new<%/if>">
+       <a name="reply-<% reply.id|html>"></a>
        <div class="reply-time hidden"><% reply.time|html></div>
+       <div class="reply-author hidden"><% reply.sone.id|html></div>
        <div class="avatar">
                <img src="/WoT/GetIdenticon?identity=<% reply.sone.id|html>&amp;width=36&height=36" width="36" height="36" alt="Avatar Image" />
        </div>
        <div class="inner-part">
                <div>
                        <div class="author profile-link"><a href="viewSone.html?sone=<% reply.sone.id|html>"><% reply.sone.niceName|html></a></div>
-                       <div class="text"><% reply.text></div>
+                       <div class="reply-text raw-text<%if !raw> hidden<%/if>"><% reply.text|html></div>
+                       <div class="reply-text text<%if raw> hidden<%/if>"><% reply.text|parse sone=reply.sone></div>
                </div>
                <div class="reply-status-line status-line">
                        <div class="time"><% reply.time|date format="MMM d, yyyy, HH:mm:ss"></div>
-                       <div class="likes<%if reply.likes.size|match value=0> hidden<%/if>"><span title="<% reply.likes.soneNames|html>">↑<span class="like-count"><% reply.likes.size></span></span></div>
+                       <span class='separator'>·</span>
+                       <div class="show-reply-source"><a href="viewPost.html?post=<% post.id|html>&amp;raw=<%if raw>false<%else>true<%/if>"><%= View.Post.ShowSource|l10n|html></a></div>
+                       <div class="likes<%if reply.likes.size|match value=0> hidden<%/if>">
+                               <span class='separator'>·</span>
+                               <span title="<% reply.likes.soneNames|html>">↑<span class="like-count"><% reply.likes.size></span></span>
+                       </div>
                        <%ifnull ! currentSone>
+                               <span class='separator'>·</span>
                                <form class="like like-reply<%if reply.liked> hidden<%/if>" action="like.html" method="post">
                                        <input type="hidden" name="formPassword" value="<% formPassword|html>" />
                                        <input type="hidden" name="returnPage" value="<% request.uri|html>" />
                                        <input type="hidden" name="reply" value="<% reply.id|html>" />
                                        <button type="submit" value="1"><%= View.Post.UnlikeLink|l10n|html></button>
                                </form>
+                               <%if !reply.sone.current>
+                                       <span class='separator'>·</span>
+                                       <form class="trust reply-trust<%if reply.sone.trust.assigned> hidden<%/if>" action="trust.html" method="post">
+                                               <input type="hidden" name="formPassword" value="<% formPassword|html>" />
+                                               <input type="hidden" name="returnPage" value="<% request.uri|html>" />
+                                               <input type="hidden" name="sone" value="<% reply.sone.id|html>" />
+                                               <button type="submit" title="<%= View.Trust.Tooltip.Trust|l10n|html>">✓</button>
+                                       </form>
+                                       <form class="distrust reply-distrust<%if reply.sone.trust.assigned> hidden<%/if>" action="distrust.html" method="post">
+                                               <input type="hidden" name="formPassword" value="<% formPassword|html>" />
+                                               <input type="hidden" name="returnPage" value="<% request.uri|html>" />
+                                               <input type="hidden" name="sone" value="<% reply.sone.id|html>" />
+                                               <button type="submit" title="<%= View.Trust.Tooltip.Distrust|l10n|html>">✗</button>
+                                       </form>
+                                       <form class="untrust reply-untrust<%if !reply.sone.trust.assigned> hidden<%/if>" action="untrust.html" method="post">
+                                               <input type="hidden" name="formPassword" value="<% formPassword|html>" />
+                                               <input type="hidden" name="returnPage" value="<% request.uri|html>" />
+                                               <input type="hidden" name="sone" value="<% reply.sone.id|html>" />
+                                               <button type="submit" title="<%= View.Trust.Tooltip.Untrust|l10n|html>">↶</button>
+                                       </form>
+                               <%/if>
                        <%/if>
-                       <%if reply.sone.current>
+                       <%if reply.sone.local>
+                               <span class='separator'>·</span>
                                <form class="delete delete-reply" action="deleteReply.html" method="post">
                                        <input type="hidden" name="formPassword" value="<% formPassword|html>" />
                                        <input type="hidden" name="returnPage" value="<% request.uri|html>" />
index 8b62fa4..c403b0d 100644 (file)
@@ -5,7 +5,7 @@
        <div class="download-marker" title="<%= View.Sone.Status.Downloading|l10n|html>">⬊</div>
        <div class="insert-marker" title="<%= View.Sone.Status.Inserting|l10n|html>">⬈</div>
        <div class="idle-marker" title="<%= View.Sone.Status.Idle|l10n|html>">✔</div>
-       <div class="last-update"><%= View.Sone.Label.LastUpdate|l10n|html> <span class="time"><% sone.time|date format="MMM d, yyyy, HH:mm:ss"></span></div>
+       <div class="last-update"><%= View.Sone.Label.LastUpdate|l10n|html> <span class="time"><% sone.time|unknown|date format="MMM d, yyyy, HH:mm:ss"></span></div>
        <div class="profile-link"><a href="viewSone.html?sone=<% sone.id|html>" title="<% sone.requestUri|html>"><% sone.niceName|html></a></div>
        <div class="short-request-uri"><% sone.requestUri|substring start=4 length=43|html></div>
        <div class="hidden"><% sone.blacklisted></div>
index 39d53f1..59bfee7 100644 (file)
@@ -4,9 +4,9 @@
 
        <h1><%= Page.Index.PostList.Title|l10n|html></h1>
 
+       <%include include/updateStatus.html>
+
        <div id="posts">
-               <%getpage>
-               <%paginate list=posts pagesize=25>
                <%= page|store key=pageParameter>
                <%include include/pagination.html>
                <%foreach pagination.items post>
index 1f12d90..69f647d 100644 (file)
                <birth-day><% currentSone.profile.birthDay|xml></birth-day>
                <birth-month><% currentSone.profile.birthMonth|xml></birth-month>
                <birth-year><% currentSone.profile.birthYear|xml></birth-year>
+               <fields>
+                       <%foreach currentSone.profile.fields field>
+                       <field>
+                               <field-name><% field.name|xml></field-name>
+                               <field-value><% field.value|xml></field-value>
+                       </field>
+                       <%/foreach>
+               </fields>
        </profile>
 
        <posts>
diff --git a/src/main/resources/templates/invalid.html b/src/main/resources/templates/invalid.html
new file mode 100644 (file)
index 0000000..d838134
--- /dev/null
@@ -0,0 +1,7 @@
+<%include include/head.html>
+
+       <h1><%= Page.Invalid.Page.Title|l10n|html></h1>
+
+       <p><%= Page.Invalid.Text|l10n|html|replace needle="{link}" replacement='<a href="index.html">'|replace needle="{/link}" replacement='</a>'></p>
+
+<%include include/tail.html>
index f5907a0..33f0e27 100644 (file)
@@ -5,8 +5,6 @@
        <h1><%= Page.KnownSones.Page.Title|l10n|html></h1>
 
        <div id="known-sones">
-               <%getpage parameter=page>
-               <%paginate list=knownSones pagesize=25>
                <%= page|store key=pageParameter>
                <%include include/pagination.html>
                <%foreach pagination.items sone>
index 94f7a1f..177b470 100644 (file)
@@ -1,10 +1,17 @@
-<div class="short-text">
+<div class="short-text hidden">
        <%= Notification.NewPost.ShortText|l10n|html>
        <a class="link" onclick="showNotificationDetails('<%notification.id|html>'); return false;"><%= Notification.ClickHereToRead|l10n|html></a>
 </div>
-<div class="text hidden">
+<div class="text">
+       <form class="mark-as-read" action="markAsKnown.html" method="post">
+               <input type="hidden" name="formPassword" value="<% formPassword|html>" />
+               <input type="hidden" name="returnPage" value="<% request.uri|html>" />
+               <input type="hidden" name="type" value="post" />
+               <input type="hidden" name="id" value="<%foreach posts post><% post.id|html><%notlast> <%/notlast><%/foreach>" />
+               <button type="submit" name="mark-read" value="true"><%= Notification.NewPost.Button.MarkRead|l10n|html></button>
+       </form>
        <%= Notification.NewPost.Text|l10n|html>
        <%foreach posts post>
-               <a href="viewPost.html?post=<% post.id|html>"><% post.sone.niceName|html></a><%notlast>,<%/notlast><%last>.<%/last>
+               <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 d6d0670..21a5ca0 100644 (file)
@@ -1,10 +1,17 @@
-<div class="short-text">
+<div class="short-text hidden">
        <%= Notification.NewReply.ShortText|l10n|html>
        <a class="link" onclick="showNotificationDetails('<%notification.id|html>'); return false;"><%= Notification.ClickHereToRead|l10n|html></a>
 </div>
-<div class="text hidden">
+<div class="text">
+       <form class="mark-as-read" action="markAsKnown.html" method="post">
+               <input type="hidden" name="formPassword" value="<% formPassword|html>" />
+               <input type="hidden" name="returnPage" value="<% request.uri|html>" />
+               <input type="hidden" name="type" value="reply" />
+               <input type="hidden" name="id" value="<%foreach replies reply><% reply.id|html><%notlast> <%/notlast><%/foreach>" />
+               <button type="submit" name="mark-read" value="true"><%= Notification.NewPost.Button.MarkRead|l10n|html></button>
+       </form>
        <%= Notification.NewReply.Text|l10n|html>
        <%foreach replies reply>
-               <a href="viewPost.html?post=<% reply.post.id|html>"><% reply.sone.niceName|html></a><%notlast>,<%/notlast><%last>.<%/last>
+               <a class="link-<% reply.post.id|html>" href="viewPost.html?post=<% reply.post.id|html>"><% reply.sone.niceName|html></a><%notlast>,<%/notlast><%last>.<%/last>
        <%/foreach>
 </div>
index 8389f7a..45357b1 100644 (file)
@@ -1,8 +1,15 @@
-<div class="short-text">
+<div class="short-text hidden">
        <%= Notification.NewSone.ShortText|l10n|html>
        <a class="link" onclick="showNotificationDetails('<%notification.id|html>'); return false;"><%= Notification.ClickHereToRead|l10n|html></a>
 </div>
-<div class="text hidden">
+<div class="text">
+       <form class="mark-as-read" action="markAsKnown.html" method="post">
+               <input type="hidden" name="formPassword" value="<% formPassword|html>" />
+               <input type="hidden" name="returnPage" value="<% request.uri|html>" />
+               <input type="hidden" name="type" value="sone" />
+               <input type="hidden" name="id" value="<%foreach sones sone><% sone.id|html><%notlast> <%/notlast><%/foreach>" />
+               <button type="submit" name="mark-read" value="true"><%= Notification.NewPost.Button.MarkRead|l10n|html></button>
+       </form>
        <%= Notification.NewSone.Text|l10n|html>
        <%foreach sones sone>
                <a href="viewSone.html?sone=<% sone.id|html>" title="<% sone.requestUri|html>"><% sone.niceName|html></a><%notlast>,<%/notlast><%last>.<%/last>
index 23a5855..5dfc25a 100644 (file)
@@ -1 +1 @@
-<div class="text"><%= Notification.NewVersion.Text|l10n|html|replace needle="{version}" replacementKey=version></div>
+<div class="text"><%= Notification.NewVersion.Text|l10n|replace needle="{version}" replacementKey=latestVersion|replace needle="{edition}" replacementKey=latestEdition|parse sone="nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI"></div>
index f0baa50..5c47764 100644 (file)
                <p><%= Page.Options.Option.InsertionDelay.Description|l10n|html></p>
                <p><input type="text" name="insertion-delay" value="<% insertion-delay|html>" /></p>
 
+               <h2><%= Page.Options.Section.TrustOptions.Title|l10n|html></h2>
+
+               <p><%= Page.Options.Option.PositiveTrust.Description|l10n|html></p>
+               <p><input type="text" name="positive-trust" value="<% positive-trust|html>" /></p>
+
+               <p><%= Page.Options.Option.NegativeTrust.Description|l10n|html></p>
+               <p><input type="text" name="negative-trust" value="<% negative-trust|html>" /></p>
+
+               <p><%= Page.Options.Option.TrustComment.Description|l10n|html></p>
+               <p><input type="text" name="trust-comment" value="<% trust-comment|html>" /></p>
+
                <h2><%= Page.Options.Section.RescueOptions.Title|l10n|html></h2>
 
                <p><%= Page.Options.Option.SoneRescueMode.Description|l10n|html></p>
index a61f858..836bf62 100644 (file)
@@ -8,7 +8,7 @@
 
                <p><%= Page.ViewPost.Text.UnknownPost|l10n|html></p>
        <%else>
-               <h1><%= Page.ViewPost.Page.Title|l10n|insert needle="{sone}" key=post.sone.niceName|html></h1>
+               <h1><%= Page.ViewPost.Page.Title|l10n|replace needle="{sone}" replacementKey=post.sone.niceName|html></h1>
 
                <%include include/viewPost.html>
        <%/if>
index 884fa0f..d2a8f6e 100644 (file)
@@ -7,7 +7,7 @@
 
                <h1><%= Page.ViewSone.Page.TitleWithoutSone|l10n|html></h1>
 
-               <p><%= Page.ViewSone.NoSone.Description|l10n|insert needle="{sone}" key=sone.id|html></p>
+               <p><%= Page.ViewSone.NoSone.Description|l10n|replace needle="{sone}" replacementKey=sone.id|html></p>
 
        <%elseifnull sone.name>
 
 
                <%if ! sone.current>
                        <%include include/viewSone.html>
+               <%/if>
 
-                       <p><%= Page.ViewSone.WriteAMessage|l10n|html></p>
+               <h1><%= Page.ViewSone.Profile.Title|l10n|html></h1>
 
-                       <form action="createPost.html" id="post-message" method="post">
-                               <input type="hidden" name="formPassword" value="<% formPassword|html>" />
-                               <input type="hidden" name="returnPage" value="<% request.uri|html>" />
-                               <input type="hidden" name="recipient" value="<% sone.id|html>" />
-                               <input type="text" name="text" value="" />
-                               <button type="submit"><%= Page.CreatePost.Button.Post|l10n|html></button>
-                       </form>
-               <%/if>
+                       <div class="profile-field">
+                               <div class="name"><%= Page.ViewSone.Profile.Label.Name|l10n|html></div>
+                               <div class="value"><a href="/WoT/ShowIdentity?id=<% sone.id|html>"><% sone.niceName|html></a></div>
+                       </div>
 
+                       <%foreach sone.profile.fields field>
+                               <div class="profile-field">
+                                       <div class="name"><% field.name|html></div>
+                                       <div class="value"><% field.value|parse></div>
+                               </div>
+                       <%/foreach>
+
+               <%ifnull ! currentSone>
+                       <%if ! sone.current>
+                               <p><%= Page.ViewSone.WriteAMessage|l10n|html></p>
+
+                               <form action="createPost.html" id="post-message" method="post">
+                                       <input type="hidden" name="formPassword" value="<% formPassword|html>" />
+                                       <input type="hidden" name="returnPage" value="<% request.uri|html>" />
+                                       <input type="hidden" name="recipient" value="<% sone.id|html>" />
+                                       <label for="sender"><%= Page.Index.Label.Sender|l10n|html></label>
+                                       <div class="sender">
+                                               <select name="sender" title="<%= View.UpdateStatus.Text.ChooseSenderIdentity|l10n|html>">
+                                                       <%foreach localSones localSone>
+                                                               <option value="<% localSone.id|html>"<%if localSone.current> selected="selected"<%/if>><% localSone.niceName|html></option>
+                                                       <%/foreach>
+                                               </select>
+                                       </div>
+                                       <div class="select-sender"><button type="button" title="<%= View.UpdateStatus.Text.ChooseSenderIdentity|l10n|html>">+</button></div><label for="text"><%= Page.Index.Label.Text|l10n|html></label>
+                                       <input type="text" name="text" value="" />
+                                       <button type="submit"><%= Page.CreatePost.Button.Post|l10n|html></button>
+                               </form>
+                       <%/if>
+               <%/if>
 
-               <h1><%= Page.ViewSone.PostList.Title|l10n|insert needle="{sone}" key=sone.niceName|html></h1>
+               <h1><%= Page.ViewSone.PostList.Title|l10n|replace needle="{sone}" replacementKey=sone.niceName|html></h1>
 
                <div id="posts">
-                       <%getpage parameter=postPage>
-                       <%paginate list=sone.posts pagesize=25>
+                       <%:getpage parameter=postPage>
+                       <%:paginate list=sone.posts pagesize=25>
                        <%= postPage|store key=pageParameter>
                        <%include include/pagination.html>
                        <%foreach pagination.items post>