Merge branch 'release-0.6.5'
[Sone.git] / src / main / java / net / pterodactylus / sone / web / WebInterface.java
index 0df6d79..73436c7 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * FreenetSone - WebInterface.java - Copyright © 2010 David Roden
+ * Sone - WebInterface.java - Copyright © 2010 David Roden
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -17,6 +17,7 @@
 
 package net.pterodactylus.sone.web;
 
+import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.Reader;
@@ -46,18 +47,22 @@ import net.pterodactylus.sone.main.SonePlugin;
 import net.pterodactylus.sone.notify.ListNotification;
 import net.pterodactylus.sone.template.CollectionAccessor;
 import net.pterodactylus.sone.template.CssClassNameFilter;
-import net.pterodactylus.sone.template.GetPagePlugin;
+import net.pterodactylus.sone.template.HttpRequestAccessor;
 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.ReplyGroupFilter;
 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.text.Part;
+import net.pterodactylus.sone.text.SonePart;
+import net.pterodactylus.sone.text.SoneTextParser;
+import net.pterodactylus.sone.web.ajax.BookmarkAjaxPage;
 import net.pterodactylus.sone.web.ajax.CreatePostAjaxPage;
 import net.pterodactylus.sone.web.ajax.CreateReplyAjaxPage;
 import net.pterodactylus.sone.web.ajax.DeletePostAjaxPage;
@@ -68,37 +73,45 @@ import net.pterodactylus.sone.web.ajax.DistrustAjaxPage;
 import net.pterodactylus.sone.web.ajax.EditProfileFieldAjaxPage;
 import net.pterodactylus.sone.web.ajax.FollowSoneAjaxPage;
 import net.pterodactylus.sone.web.ajax.GetLikesAjaxPage;
+import net.pterodactylus.sone.web.ajax.GetNotificationAjaxPage;
 import net.pterodactylus.sone.web.ajax.GetPostAjaxPage;
 import net.pterodactylus.sone.web.ajax.GetReplyAjaxPage;
 import net.pterodactylus.sone.web.ajax.GetStatusAjaxPage;
+import net.pterodactylus.sone.web.ajax.GetTimesAjaxPage;
 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.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.RedirectPage;
 import net.pterodactylus.sone.web.page.StaticPage;
+import net.pterodactylus.sone.web.page.TemplatePage;
 import net.pterodactylus.util.cache.Cache;
 import net.pterodactylus.util.cache.CacheException;
 import net.pterodactylus.util.cache.CacheItem;
 import net.pterodactylus.util.cache.DefaultCacheItem;
 import net.pterodactylus.util.cache.MemoryCache;
 import net.pterodactylus.util.cache.ValueRetriever;
+import net.pterodactylus.util.collection.SetBuilder;
+import net.pterodactylus.util.filter.Filters;
 import net.pterodactylus.util.logging.Logging;
 import net.pterodactylus.util.notify.Notification;
 import net.pterodactylus.util.notify.NotificationManager;
 import net.pterodactylus.util.notify.TemplateNotification;
+import net.pterodactylus.util.template.CollectionSortFilter;
+import net.pterodactylus.util.template.ContainsFilter;
 import net.pterodactylus.util.template.DateFilter;
 import net.pterodactylus.util.template.FormatFilter;
 import net.pterodactylus.util.template.HtmlFilter;
 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;
@@ -116,6 +129,7 @@ import freenet.clients.http.SessionManager.Session;
 import freenet.clients.http.ToadletContainer;
 import freenet.clients.http.ToadletContext;
 import freenet.l10n.BaseL10n;
+import freenet.support.api.HTTPRequest;
 
 /**
  * Bundles functionality that a web interface of a Freenet plugin needs, e.g.
@@ -143,6 +157,9 @@ public class WebInterface implements CoreListener {
        /** The template context factory. */
        private final TemplateContextFactory templateContextFactory;
 
+       /** The Sone text parser. */
+       private final SoneTextParser soneTextParser;
+
        /** The “new Sone” notification. */
        private final ListNotification<Sone> newSoneNotification;
 
@@ -152,6 +169,15 @@ public class WebInterface implements CoreListener {
        /** The “new reply” notification. */
        private final ListNotification<Reply> newReplyNotification;
 
+       /** The invisible “local post” notification. */
+       private final ListNotification<Post> localPostNotification;
+
+       /** The invisible “local reply” notification. */
+       private final ListNotification<Reply> localReplyNotification;
+
+       /** The “you have been mentioned” notification. */
+       private final ListNotification<Post> mentionNotification;
+
        /** The “rescuing Sone” notification. */
        private final ListNotification<Sone> rescuingSonesNotification;
 
@@ -177,6 +203,7 @@ public class WebInterface implements CoreListener {
        public WebInterface(SonePlugin sonePlugin) {
                this.sonePlugin = sonePlugin;
                formPassword = sonePlugin.pluginRespirator().getToadletContainer().getFormPassword();
+               soneTextParser = new SoneTextParser(getCore(), getCore());
 
                templateContextFactory = new TemplateContextFactory();
                templateContextFactory.addAccessor(Object.class, new ReflectionAccessor());
@@ -185,8 +212,8 @@ public class WebInterface implements CoreListener {
                templateContextFactory.addAccessor(Post.class, new PostAccessor(getCore()));
                templateContextFactory.addAccessor(Reply.class, new ReplyAccessor(getCore()));
                templateContextFactory.addAccessor(Identity.class, new IdentityAccessor(getCore()));
-               templateContextFactory.addAccessor(NotificationManager.class, new NotificationManagerAccessor());
                templateContextFactory.addAccessor(Trust.class, new TrustAccessor());
+               templateContextFactory.addAccessor(HTTPRequest.class, new HttpRequestAccessor());
                templateContextFactory.addFilter("date", new DateFilter());
                templateContextFactory.addFilter("html", new HtmlFilter());
                templateContextFactory.addFilter("replace", new ReplaceFilter());
@@ -198,24 +225,35 @@ public class WebInterface implements CoreListener {
                templateContextFactory.addFilter("match", new MatchFilter());
                templateContextFactory.addFilter("css", new CssClassNameFilter());
                templateContextFactory.addFilter("js", new JavascriptFilter());
-               templateContextFactory.addFilter("parse", new ParserFilter(templateContextFactory));
+               templateContextFactory.addFilter("parse", new ParserFilter(getCore(), templateContextFactory, soneTextParser));
                templateContextFactory.addFilter("unknown", new UnknownDateFilter(getL10n(), "View.Sone.Text.UnknownDate"));
                templateContextFactory.addFilter("format", new FormatFilter());
-               templateContextFactory.addPlugin("getpage", new GetPagePlugin());
-               templateContextFactory.addPlugin("paginate", new PaginationPlugin());
+               templateContextFactory.addFilter("sort", new CollectionSortFilter());
+               templateContextFactory.addFilter("replyGroup", new ReplyGroupFilter());
+               templateContextFactory.addFilter("in", new ContainsFilter());
                templateContextFactory.addProvider(Provider.TEMPLATE_CONTEXT_PROVIDER);
                templateContextFactory.addProvider(new ClassPathTemplateProvider());
+               templateContextFactory.addTemplateObject("webInterface", this);
                templateContextFactory.addTemplateObject("formPassword", formPassword);
 
                /* create notifications. */
                Template newSoneNotificationTemplate = TemplateParser.parse(createReader("/templates/notify/newSoneNotification.html"));
-               newSoneNotification = new ListNotification<Sone>("new-sone-notification", "sones", newSoneNotificationTemplate);
+               newSoneNotification = new ListNotification<Sone>("new-sone-notification", "sones", newSoneNotificationTemplate, false);
 
                Template newPostNotificationTemplate = TemplateParser.parse(createReader("/templates/notify/newPostNotification.html"));
-               newPostNotification = new ListNotification<Post>("new-post-notification", "posts", newPostNotificationTemplate);
+               newPostNotification = new ListNotification<Post>("new-post-notification", "posts", newPostNotificationTemplate, false);
+
+               Template localPostNotificationTemplate = TemplateParser.parse(createReader("/templates/notify/newPostNotification.html"));
+               localPostNotification = new ListNotification<Post>("local-post-notification", "posts", localPostNotificationTemplate, false);
 
                Template newReplyNotificationTemplate = TemplateParser.parse(createReader("/templates/notify/newReplyNotification.html"));
-               newReplyNotification = new ListNotification<Reply>("new-replies-notification", "replies", newReplyNotificationTemplate);
+               newReplyNotification = new ListNotification<Reply>("new-reply-notification", "replies", newReplyNotificationTemplate, false);
+
+               Template localReplyNotificationTemplate = TemplateParser.parse(createReader("/templates/notify/newReplyNotification.html"));
+               localReplyNotification = new ListNotification<Reply>("local-reply-notification", "replies", localReplyNotificationTemplate, false);
+
+               Template mentionNotificationTemplate = TemplateParser.parse(createReader("/templates/notify/mentionNotification.html"));
+               mentionNotification = new ListNotification<Post>("mention-notification", "posts", mentionNotificationTemplate, false);
 
                Template rescuingSonesTemplate = TemplateParser.parse(createReader("/templates/notify/rescuingSonesNotification.html"));
                rescuingSonesNotification = new ListNotification<Sone>("sones-being-rescued-notification", "sones", rescuingSonesTemplate);
@@ -395,7 +433,7 @@ public class WebInterface implements CoreListener {
         * @return The new posts
         */
        public Set<Post> getNewPosts() {
-               return new HashSet<Post>(newPostNotification.getElements());
+               return new SetBuilder<Post>().addAll(newPostNotification.getElements()).addAll(localPostNotification.getElements()).get();
        }
 
        /**
@@ -405,7 +443,7 @@ public class WebInterface implements CoreListener {
         * @return The new replies
         */
        public Set<Reply> getNewReplies() {
-               return new HashSet<Reply>(newReplyNotification.getElements());
+               return new SetBuilder<Reply>().addAll(newReplyNotification.getElements()).addAll(localReplyNotification.getElements()).get();
        }
 
        /**
@@ -518,6 +556,8 @@ public class WebInterface implements CoreListener {
                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 searchTemplate = TemplateParser.parse(createReader("/templates/search.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"));
@@ -532,8 +572,10 @@ public class WebInterface implements CoreListener {
                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"));
+               Template openSearchTemplate = TemplateParser.parse(createReader("/templates/xml/OpenSearch.xml"));
 
                PageToadletFactory pageToadletFactory = new PageToadletFactory(sonePlugin.pluginRespirator().getHLSimpleClient(), "/Sone/");
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new RedirectPage("", "index.html")));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new IndexPage(indexTemplate, this), "Index"));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new CreateSonePage(createSoneTemplate, this), "CreateSone"));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new KnownSonesPage(knownSonesTemplate, this), "KnownSones"));
@@ -556,6 +598,10 @@ public class WebInterface implements CoreListener {
                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 SearchPage(searchTemplate, this)));
                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"));
@@ -567,13 +613,16 @@ public class WebInterface implements CoreListener {
                pageToadlets.add(pageToadletFactory.createPageToadlet(new StaticPage("css/", "/static/css/", "text/css")));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new StaticPage("javascript/", "/static/javascript/", "text/javascript")));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new StaticPage("images/", "/static/images/", "image/png")));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new TemplatePage("OpenSearch.xml", "application/opensearchdescription+xml", templateContextFactory, openSearchTemplate)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new GetTranslationPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new GetStatusAjaxPage(this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new GetNotificationAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new DismissNotificationAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new CreatePostAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new CreateReplyAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new GetReplyAjaxPage(this, replyTemplate)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new GetPostAjaxPage(this, postTemplate)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new GetTimesAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new MarkAsKnownAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new DeletePostAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new DeleteReplyAjaxPage(this)));
@@ -587,6 +636,8 @@ public class WebInterface implements CoreListener {
                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)));
@@ -626,11 +677,34 @@ public class WebInterface implements CoreListener {
                try {
                        return new InputStreamReader(getClass().getResourceAsStream(resourceName), "UTF-8");
                } catch (UnsupportedEncodingException uee1) {
-                       System.out.println("  fail.");
                        return null;
                }
        }
 
+       /**
+        * Returns all {@link Core#isLocalSone(Sone) local Sone}s that are
+        * referenced by {@link SonePart}s in the given text (after parsing it using
+        * {@link SoneTextParser}).
+        *
+        * @param text
+        *            The text to parse
+        * @return All mentioned local Sones
+        */
+       private Set<Sone> getMentionedSones(String text) {
+               /* we need no context to find mentioned Sones. */
+               Set<Sone> mentionedSones = new HashSet<Sone>();
+               try {
+                       for (Part part : soneTextParser.parse(null, new StringReader(text))) {
+                               if (part instanceof SonePart) {
+                                       mentionedSones.add(((SonePart) part).getSone());
+                               }
+                       }
+               } catch (IOException ioe1) {
+                       logger.log(Level.WARNING, "Could not parse post text: " + text, ioe1);
+               }
+               return Filters.filteredSet(mentionedSones, Sone.LOCAL_SONE_FILTER);
+       }
+
        //
        // CORELISTENER METHODS
        //
@@ -670,9 +744,18 @@ public class WebInterface implements CoreListener {
         */
        @Override
        public void newPostFound(Post post) {
-               newPostNotification.add(post);
+               boolean isLocal = getCore().isLocalSone(post.getSone());
+               if (isLocal) {
+                       localPostNotification.add(post);
+               } else {
+                       newPostNotification.add(post);
+               }
                if (!hasFirstStartNotification()) {
-                       notificationManager.addNotification(newPostNotification);
+                       notificationManager.addNotification(isLocal ? localPostNotification : newPostNotification);
+                       if (!getMentionedSones(post.getText()).isEmpty() && !isLocal) {
+                               mentionNotification.add(post);
+                               notificationManager.addNotification(mentionNotification);
+                       }
                } else {
                        getCore().markPostKnown(post);
                }
@@ -686,9 +769,18 @@ public class WebInterface implements CoreListener {
                if (reply.getPost().getSone() == null) {
                        return;
                }
-               newReplyNotification.add(reply);
+               boolean isLocal = getCore().isLocalSone(reply.getSone());
+               if (isLocal) {
+                       localReplyNotification.add(reply);
+               } else {
+                       newReplyNotification.add(reply);
+               }
                if (!hasFirstStartNotification()) {
-                       notificationManager.addNotification(newReplyNotification);
+                       notificationManager.addNotification(isLocal ? localReplyNotification : newReplyNotification);
+                       if (!getMentionedSones(reply.getText()).isEmpty() && !isLocal) {
+                               mentionNotification.add(reply.getPost());
+                               notificationManager.addNotification(mentionNotification);
+                       }
                } else {
                        getCore().markReplyKnown(reply);
                }
@@ -708,6 +800,8 @@ public class WebInterface implements CoreListener {
        @Override
        public void markPostKnown(Post post) {
                newPostNotification.remove(post);
+               localPostNotification.remove(post);
+               mentionNotification.remove(post);
        }
 
        /**
@@ -716,6 +810,16 @@ public class WebInterface implements CoreListener {
        @Override
        public void markReplyKnown(Reply reply) {
                newReplyNotification.remove(reply);
+               localReplyNotification.remove(reply);
+               mentionNotification.remove(reply.getPost());
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public void soneRemoved(Sone sone) {
+               newSoneNotification.remove(sone);
        }
 
        /**
@@ -724,6 +828,7 @@ public class WebInterface implements CoreListener {
        @Override
        public void postRemoved(Post post) {
                newPostNotification.remove(post);
+               localPostNotification.remove(post);
        }
 
        /**
@@ -732,6 +837,7 @@ public class WebInterface implements CoreListener {
        @Override
        public void replyRemoved(Reply reply) {
                newReplyNotification.remove(reply);
+               localReplyNotification.remove(reply);
        }
 
        /**
@@ -765,8 +871,9 @@ public class WebInterface implements CoreListener {
         * {@inheritDoc}
         */
        @Override
-       public void updateFound(Version version, long releaseTime) {
-               newVersionNotification.getTemplateContext().set("version", version);
+       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);
        }