🚧 Add Loaders to all template-using pages
[Sone.git] / src / main / java / net / pterodactylus / sone / web / WebInterface.java
1 /*
2  * Sone - WebInterface.java - Copyright Â© 2010–2019 David Roden
3  *
4  * This program is free software: you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation, either version 3 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
16  */
17
18 package net.pterodactylus.sone.web;
19
20 import static com.google.common.collect.FluentIterable.from;
21 import static java.util.logging.Logger.getLogger;
22 import static net.pterodactylus.util.template.TemplateParser.parse;
23
24 import java.io.StringReader;
25 import java.util.Collection;
26 import java.util.Collections;
27 import java.util.HashMap;
28 import java.util.HashSet;
29 import java.util.Map;
30 import java.util.Set;
31 import java.util.TimeZone;
32 import java.util.UUID;
33 import java.util.concurrent.Executors;
34 import java.util.concurrent.ScheduledExecutorService;
35 import java.util.concurrent.ScheduledFuture;
36 import java.util.concurrent.TimeUnit;
37 import java.util.logging.Logger;
38 import javax.annotation.Nonnull;
39 import javax.annotation.Nullable;
40
41 import net.pterodactylus.sone.core.Core;
42 import net.pterodactylus.sone.core.ElementLoader;
43 import net.pterodactylus.sone.core.event.ImageInsertAbortedEvent;
44 import net.pterodactylus.sone.core.event.ImageInsertFailedEvent;
45 import net.pterodactylus.sone.core.event.ImageInsertFinishedEvent;
46 import net.pterodactylus.sone.core.event.ImageInsertStartedEvent;
47 import net.pterodactylus.sone.core.event.MarkPostKnownEvent;
48 import net.pterodactylus.sone.core.event.MarkPostReplyKnownEvent;
49 import net.pterodactylus.sone.core.event.MarkSoneKnownEvent;
50 import net.pterodactylus.sone.core.event.NewPostFoundEvent;
51 import net.pterodactylus.sone.core.event.NewPostReplyFoundEvent;
52 import net.pterodactylus.sone.core.event.NewSoneFoundEvent;
53 import net.pterodactylus.sone.core.event.PostRemovedEvent;
54 import net.pterodactylus.sone.core.event.PostReplyRemovedEvent;
55 import net.pterodactylus.sone.core.event.SoneInsertAbortedEvent;
56 import net.pterodactylus.sone.core.event.SoneInsertedEvent;
57 import net.pterodactylus.sone.core.event.SoneInsertingEvent;
58 import net.pterodactylus.sone.core.event.SoneLockedEvent;
59 import net.pterodactylus.sone.core.event.SoneRemovedEvent;
60 import net.pterodactylus.sone.core.event.SoneUnlockedEvent;
61 import net.pterodactylus.sone.core.event.UpdateFoundEvent;
62 import net.pterodactylus.sone.data.Image;
63 import net.pterodactylus.sone.data.Post;
64 import net.pterodactylus.sone.data.PostReply;
65 import net.pterodactylus.sone.data.Sone;
66 import net.pterodactylus.sone.freenet.L10nFilter;
67 import net.pterodactylus.sone.main.Loaders;
68 import net.pterodactylus.sone.main.PluginHomepage;
69 import net.pterodactylus.sone.main.PluginVersion;
70 import net.pterodactylus.sone.main.PluginYear;
71 import net.pterodactylus.sone.main.SonePlugin;
72 import net.pterodactylus.sone.notify.ListNotification;
73 import net.pterodactylus.sone.notify.ListNotificationFilter;
74 import net.pterodactylus.sone.notify.PostVisibilityFilter;
75 import net.pterodactylus.sone.notify.ReplyVisibilityFilter;
76 import net.pterodactylus.sone.template.LinkedElementRenderFilter;
77 import net.pterodactylus.sone.template.ParserFilter;
78 import net.pterodactylus.sone.template.RenderFilter;
79 import net.pterodactylus.sone.template.ShortenFilter;
80 import net.pterodactylus.sone.text.Part;
81 import net.pterodactylus.sone.text.SonePart;
82 import net.pterodactylus.sone.text.SoneTextParser;
83 import net.pterodactylus.sone.text.TimeTextConverter;
84 import net.pterodactylus.sone.web.ajax.BookmarkAjaxPage;
85 import net.pterodactylus.sone.web.ajax.CreatePostAjaxPage;
86 import net.pterodactylus.sone.web.ajax.CreateReplyAjaxPage;
87 import net.pterodactylus.sone.web.ajax.DeletePostAjaxPage;
88 import net.pterodactylus.sone.web.ajax.DeleteProfileFieldAjaxPage;
89 import net.pterodactylus.sone.web.ajax.DeleteReplyAjaxPage;
90 import net.pterodactylus.sone.web.ajax.DismissNotificationAjaxPage;
91 import net.pterodactylus.sone.web.ajax.DistrustAjaxPage;
92 import net.pterodactylus.sone.web.ajax.EditAlbumAjaxPage;
93 import net.pterodactylus.sone.web.ajax.EditImageAjaxPage;
94 import net.pterodactylus.sone.web.ajax.EditProfileFieldAjaxPage;
95 import net.pterodactylus.sone.web.ajax.FollowSoneAjaxPage;
96 import net.pterodactylus.sone.web.ajax.GetLikesAjaxPage;
97 import net.pterodactylus.sone.web.ajax.GetLinkedElementAjaxPage;
98 import net.pterodactylus.sone.web.ajax.GetNotificationsAjaxPage;
99 import net.pterodactylus.sone.web.ajax.GetPostAjaxPage;
100 import net.pterodactylus.sone.web.ajax.GetReplyAjaxPage;
101 import net.pterodactylus.sone.web.ajax.GetStatusAjaxPage;
102 import net.pterodactylus.sone.web.ajax.GetTimesAjaxPage;
103 import net.pterodactylus.sone.web.ajax.GetTranslationAjaxPage;
104 import net.pterodactylus.sone.web.ajax.LikeAjaxPage;
105 import net.pterodactylus.sone.web.ajax.LockSoneAjaxPage;
106 import net.pterodactylus.sone.web.ajax.MarkAsKnownAjaxPage;
107 import net.pterodactylus.sone.web.ajax.MoveProfileFieldAjaxPage;
108 import net.pterodactylus.sone.web.ajax.TrustAjaxPage;
109 import net.pterodactylus.sone.web.ajax.UnbookmarkAjaxPage;
110 import net.pterodactylus.sone.web.ajax.UnfollowSoneAjaxPage;
111 import net.pterodactylus.sone.web.ajax.UnlikeAjaxPage;
112 import net.pterodactylus.sone.web.ajax.UnlockSoneAjaxPage;
113 import net.pterodactylus.sone.web.ajax.UntrustAjaxPage;
114 import net.pterodactylus.sone.web.page.FreenetRequest;
115 import net.pterodactylus.sone.web.pages.AboutPage;
116 import net.pterodactylus.sone.web.pages.BookmarkPage;
117 import net.pterodactylus.sone.web.pages.BookmarksPage;
118 import net.pterodactylus.sone.web.pages.CreateAlbumPage;
119 import net.pterodactylus.sone.web.pages.CreatePostPage;
120 import net.pterodactylus.sone.web.pages.CreateReplyPage;
121 import net.pterodactylus.sone.web.pages.CreateSonePage;
122 import net.pterodactylus.sone.web.pages.DeleteAlbumPage;
123 import net.pterodactylus.sone.web.pages.DeleteImagePage;
124 import net.pterodactylus.sone.web.pages.DeletePostPage;
125 import net.pterodactylus.sone.web.pages.DeleteProfileFieldPage;
126 import net.pterodactylus.sone.web.pages.DeleteReplyPage;
127 import net.pterodactylus.sone.web.pages.DeleteSonePage;
128 import net.pterodactylus.sone.web.pages.DismissNotificationPage;
129 import net.pterodactylus.sone.web.pages.DistrustPage;
130 import net.pterodactylus.sone.web.pages.EditAlbumPage;
131 import net.pterodactylus.sone.web.pages.EditImagePage;
132 import net.pterodactylus.sone.web.pages.EditProfileFieldPage;
133 import net.pterodactylus.sone.web.pages.EditProfilePage;
134 import net.pterodactylus.sone.web.pages.FollowSonePage;
135 import net.pterodactylus.sone.web.pages.GetImagePage;
136 import net.pterodactylus.sone.web.pages.ImageBrowserPage;
137 import net.pterodactylus.sone.web.pages.IndexPage;
138 import net.pterodactylus.sone.web.pages.KnownSonesPage;
139 import net.pterodactylus.sone.web.pages.LikePage;
140 import net.pterodactylus.sone.web.pages.LockSonePage;
141 import net.pterodactylus.sone.web.pages.LoginPage;
142 import net.pterodactylus.sone.web.pages.LogoutPage;
143 import net.pterodactylus.sone.web.pages.MarkAsKnownPage;
144 import net.pterodactylus.sone.web.pages.NewPage;
145 import net.pterodactylus.sone.web.pages.OptionsPage;
146 import net.pterodactylus.sone.web.pages.RescuePage;
147 import net.pterodactylus.sone.web.pages.SearchPage;
148 import net.pterodactylus.sone.web.pages.SoneTemplatePage;
149 import net.pterodactylus.sone.web.pages.TrustPage;
150 import net.pterodactylus.sone.web.pages.UnbookmarkPage;
151 import net.pterodactylus.sone.web.pages.UnfollowSonePage;
152 import net.pterodactylus.sone.web.pages.UnlikePage;
153 import net.pterodactylus.sone.web.pages.UnlockSonePage;
154 import net.pterodactylus.sone.web.pages.UntrustPage;
155 import net.pterodactylus.sone.web.pages.UploadImagePage;
156 import net.pterodactylus.sone.web.pages.ViewPostPage;
157 import net.pterodactylus.sone.web.pages.ViewSonePage;
158 import net.pterodactylus.util.notify.Notification;
159 import net.pterodactylus.util.notify.NotificationManager;
160 import net.pterodactylus.util.notify.TemplateNotification;
161 import net.pterodactylus.util.template.Template;
162 import net.pterodactylus.util.template.TemplateContextFactory;
163 import net.pterodactylus.util.web.RedirectPage;
164 import net.pterodactylus.util.web.TemplatePage;
165
166 import freenet.clients.http.SessionManager;
167 import freenet.clients.http.SessionManager.Session;
168 import freenet.clients.http.ToadletContext;
169 import freenet.l10n.BaseL10n;
170
171 import com.google.common.base.Optional;
172 import com.google.common.collect.Collections2;
173 import com.google.common.collect.ImmutableSet;
174 import com.google.common.eventbus.Subscribe;
175 import com.google.inject.Inject;
176
177 /**
178  * Bundles functionality that a web interface of a Freenet plugin needs, e.g.
179  * references to l10n helpers.
180  */
181 public class WebInterface implements SessionProvider {
182
183         /** The logger. */
184         private static final Logger logger = getLogger(WebInterface.class.getName());
185
186         /** The loaders for templates, pages, and classpath providers. */
187         private final Loaders loaders;
188
189         /** The notification manager. */
190         private final NotificationManager notificationManager = new NotificationManager();
191
192         /** The Sone plugin. */
193         private final SonePlugin sonePlugin;
194
195         /** The form password. */
196         private final String formPassword;
197
198         /** The template context factory. */
199         private final TemplateContextFactory templateContextFactory;
200
201         /** The Sone text parser. */
202         private final SoneTextParser soneTextParser;
203
204         /** The parser filter. */
205         private final ParserFilter parserFilter;
206         private final ShortenFilter shortenFilter;
207         private final RenderFilter renderFilter;
208
209         private final ListNotificationFilter listNotificationFilter;
210         private final PostVisibilityFilter postVisibilityFilter;
211         private final ReplyVisibilityFilter replyVisibilityFilter;
212
213         private final ElementLoader elementLoader;
214         private final LinkedElementRenderFilter linkedElementRenderFilter;
215         private final TimeTextConverter timeTextConverter = new TimeTextConverter();
216         private final L10nFilter l10nFilter;
217
218         private final PageToadletRegistry pageToadletRegistry;
219
220         /** The â€śnew Sone” notification. */
221         private final ListNotification<Sone> newSoneNotification;
222
223         /** The â€śnew post” notification. */
224         private final ListNotification<Post> newPostNotification;
225
226         /** The â€śnew reply” notification. */
227         private final ListNotification<PostReply> newReplyNotification;
228
229         /** The invisible â€ślocal post” notification. */
230         private final ListNotification<Post> localPostNotification;
231
232         /** The invisible â€ślocal reply” notification. */
233         private final ListNotification<PostReply> localReplyNotification;
234
235         /** The â€śyou have been mentioned” notification. */
236         private final ListNotification<Post> mentionNotification;
237
238         /** Notifications for sone inserts. */
239         private final Map<Sone, TemplateNotification> soneInsertNotifications = new HashMap<>();
240
241         /** Sone locked notification ticker objects. */
242         private final Map<Sone, ScheduledFuture<?>> lockedSonesTickerObjects = Collections.synchronizedMap(new HashMap<Sone, ScheduledFuture<?>>());
243
244         /** The â€śSone locked” notification. */
245         private final ListNotification<Sone> lockedSonesNotification;
246
247         /** The â€śnew version” notification. */
248         private final TemplateNotification newVersionNotification;
249
250         /** The â€śinserting images” notification. */
251         private final ListNotification<Image> insertingImagesNotification;
252
253         /** The â€śinserted images” notification. */
254         private final ListNotification<Image> insertedImagesNotification;
255
256         /** The â€śimage insert failed” notification. */
257         private final ListNotification<Image> imageInsertFailedNotification;
258
259         /** Scheduled executor for time-based notifications. */
260         private final ScheduledExecutorService ticker = Executors.newScheduledThreadPool(1);
261
262         @Inject
263         public WebInterface(SonePlugin sonePlugin, Loaders loaders, ListNotificationFilter listNotificationFilter,
264                         PostVisibilityFilter postVisibilityFilter, ReplyVisibilityFilter replyVisibilityFilter,
265                         ElementLoader elementLoader, TemplateContextFactory templateContextFactory,
266                         ParserFilter parserFilter, ShortenFilter shortenFilter,
267                         RenderFilter renderFilter,
268                         LinkedElementRenderFilter linkedElementRenderFilter,
269                         PageToadletRegistry pageToadletRegistry) {
270                 this.sonePlugin = sonePlugin;
271                 this.loaders = loaders;
272                 this.listNotificationFilter = listNotificationFilter;
273                 this.postVisibilityFilter = postVisibilityFilter;
274                 this.replyVisibilityFilter = replyVisibilityFilter;
275                 this.elementLoader = elementLoader;
276                 this.parserFilter = parserFilter;
277                 this.shortenFilter = shortenFilter;
278                 this.renderFilter = renderFilter;
279                 this.linkedElementRenderFilter = linkedElementRenderFilter;
280                 this.pageToadletRegistry = pageToadletRegistry;
281                 formPassword = sonePlugin.pluginRespirator().getToadletContainer().getFormPassword();
282                 soneTextParser = new SoneTextParser(getCore(), getCore());
283                 l10nFilter = new L10nFilter(getL10n());
284
285                 this.templateContextFactory = templateContextFactory;
286                 templateContextFactory.addTemplateObject("webInterface", this);
287                 templateContextFactory.addTemplateObject("formPassword", formPassword);
288
289                 /* create notifications. */
290                 Template newSoneNotificationTemplate = loaders.loadTemplate("/templates/notify/newSoneNotification.html");
291                 newSoneNotification = new ListNotification<>("new-sone-notification", "sones", newSoneNotificationTemplate, false);
292
293                 Template newPostNotificationTemplate = loaders.loadTemplate("/templates/notify/newPostNotification.html");
294                 newPostNotification = new ListNotification<>("new-post-notification", "posts", newPostNotificationTemplate, false);
295
296                 Template localPostNotificationTemplate = loaders.loadTemplate("/templates/notify/newPostNotification.html");
297                 localPostNotification = new ListNotification<>("local-post-notification", "posts", localPostNotificationTemplate, false);
298
299                 Template newReplyNotificationTemplate = loaders.loadTemplate("/templates/notify/newReplyNotification.html");
300                 newReplyNotification = new ListNotification<>("new-reply-notification", "replies", newReplyNotificationTemplate, false);
301
302                 Template localReplyNotificationTemplate = loaders.loadTemplate("/templates/notify/newReplyNotification.html");
303                 localReplyNotification = new ListNotification<>("local-reply-notification", "replies", localReplyNotificationTemplate, false);
304
305                 Template mentionNotificationTemplate = loaders.loadTemplate("/templates/notify/mentionNotification.html");
306                 mentionNotification = new ListNotification<>("mention-notification", "posts", mentionNotificationTemplate, false);
307
308                 Template lockedSonesTemplate = loaders.loadTemplate("/templates/notify/lockedSonesNotification.html");
309                 lockedSonesNotification = new ListNotification<>("sones-locked-notification", "sones", lockedSonesTemplate);
310
311                 Template newVersionTemplate = loaders.loadTemplate("/templates/notify/newVersionNotification.html");
312                 newVersionNotification = new TemplateNotification("new-version-notification", newVersionTemplate);
313
314                 Template insertingImagesTemplate = loaders.loadTemplate("/templates/notify/inserting-images-notification.html");
315                 insertingImagesNotification = new ListNotification<>("inserting-images-notification", "images", insertingImagesTemplate);
316
317                 Template insertedImagesTemplate = loaders.loadTemplate("/templates/notify/inserted-images-notification.html");
318                 insertedImagesNotification = new ListNotification<>("inserted-images-notification", "images", insertedImagesTemplate);
319
320                 Template imageInsertFailedTemplate = loaders.loadTemplate("/templates/notify/image-insert-failed-notification.html");
321                 imageInsertFailedNotification = new ListNotification<>("image-insert-failed-notification", "images", imageInsertFailedTemplate);
322         }
323
324         //
325         // ACCESSORS
326         //
327
328         /**
329          * Returns the Sone core used by the Sone plugin.
330          *
331          * @return The Sone core
332          */
333         @Nonnull
334         public Core getCore() {
335                 return sonePlugin.core();
336         }
337
338         /**
339          * Returns the template context factory of the web interface.
340          *
341          * @return The template context factory
342          */
343         public TemplateContextFactory getTemplateContextFactory() {
344                 return templateContextFactory;
345         }
346
347         private Session getCurrentSessionWithoutCreation(ToadletContext toadletContenxt) {
348                 return getSessionManager().useSession(toadletContenxt);
349         }
350
351         private Session getOrCreateCurrentSession(ToadletContext toadletContenxt) {
352                 Session session = getCurrentSessionWithoutCreation(toadletContenxt);
353                 if (session == null) {
354                         session = getSessionManager().createSession(UUID.randomUUID().toString(), toadletContenxt);
355                 }
356                 return session;
357         }
358
359         public Sone getCurrentSoneCreatingSession(ToadletContext toadletContext) {
360                 Collection<Sone> localSones = getCore().getLocalSones();
361                 if (localSones.size() == 1) {
362                         return localSones.iterator().next();
363                 }
364                 return getCurrentSone(getOrCreateCurrentSession(toadletContext));
365         }
366
367         public Sone getCurrentSoneWithoutCreatingSession(ToadletContext toadletContext) {
368                 Collection<Sone> localSones = getCore().getLocalSones();
369                 if (localSones.size() == 1) {
370                         return localSones.iterator().next();
371                 }
372                 return getCurrentSone(getCurrentSessionWithoutCreation(toadletContext));
373         }
374
375         /**
376          * Returns the currently logged in Sone.
377          *
378          * @param session
379          *            The session
380          * @return The currently logged in Sone, or {@code null} if no Sone is
381          *         currently logged in
382          */
383         private Sone getCurrentSone(Session session) {
384                 if (session == null) {
385                         return null;
386                 }
387                 String soneId = (String) session.getAttribute("Sone.CurrentSone");
388                 if (soneId == null) {
389                         return null;
390                 }
391                 return getCore().getLocalSone(soneId);
392         }
393
394         @Override
395         @Nullable
396         public Sone getCurrentSone(@Nonnull ToadletContext toadletContext, boolean createSession) {
397                 return createSession ? getCurrentSoneCreatingSession(toadletContext) : getCurrentSoneWithoutCreatingSession(toadletContext);
398         }
399
400         /**
401          * Sets the currently logged in Sone.
402          *
403          * @param toadletContext
404          *            The toadlet context
405          * @param sone
406          *            The Sone to set as currently logged in
407          */
408         @Override
409         public void setCurrentSone(@Nonnull ToadletContext toadletContext, @Nullable Sone sone) {
410                 Session session = getOrCreateCurrentSession(toadletContext);
411                 if (sone == null) {
412                         session.removeAttribute("Sone.CurrentSone");
413                 } else {
414                         session.setAttribute("Sone.CurrentSone", sone.getId());
415                 }
416         }
417
418         /**
419          * Returns the notification manager.
420          *
421          * @return The notification manager
422          */
423         public NotificationManager getNotifications() {
424                 return notificationManager;
425         }
426
427         @Nonnull
428         public Optional<Notification> getNotification(@Nonnull String notificationId) {
429                 return Optional.fromNullable(notificationManager.getNotification(notificationId));
430         }
431
432         @Nonnull
433         public Collection<Notification> getNotifications(@Nullable Sone currentSone) {
434                 return listNotificationFilter.filterNotifications(notificationManager.getNotifications(), currentSone);
435         }
436
437         /**
438          * Returns the l10n helper of the node.
439          *
440          * @return The node’s l10n helper
441          */
442         public BaseL10n getL10n() {
443                 return sonePlugin.l10n().getBase();
444         }
445
446         /**
447          * Returns the session manager of the node.
448          *
449          * @return The node’s session manager
450          */
451         public SessionManager getSessionManager() {
452                 return sonePlugin.pluginRespirator().getSessionManager("Sone");
453         }
454
455         /**
456          * Returns the node’s form password.
457          *
458          * @return The form password
459          */
460         public String getFormPassword() {
461                 return formPassword;
462         }
463
464         /**
465          * Returns the posts that have been announced as new in the
466          * {@link #newPostNotification}.
467          *
468          * @return The new posts
469          */
470         public Set<Post> getNewPosts() {
471                 return ImmutableSet.<Post> builder().addAll(newPostNotification.getElements()).addAll(localPostNotification.getElements()).build();
472         }
473
474         @Nonnull
475         public Collection<Post> getNewPosts(@Nullable Sone currentSone) {
476                 Set<Post> allNewPosts = ImmutableSet.<Post> builder()
477                                 .addAll(newPostNotification.getElements())
478                                 .addAll(localPostNotification.getElements())
479                                 .build();
480                 return from(allNewPosts).filter(postVisibilityFilter.isVisible(currentSone)).toSet();
481         }
482
483         /**
484          * Returns the replies that have been announced as new in the
485          * {@link #newReplyNotification}.
486          *
487          * @return The new replies
488          */
489         public Set<PostReply> getNewReplies() {
490                 return ImmutableSet.<PostReply> builder().addAll(newReplyNotification.getElements()).addAll(localReplyNotification.getElements()).build();
491         }
492
493         @Nonnull
494         public Collection<PostReply> getNewReplies(@Nullable Sone currentSone) {
495                 Set<PostReply> allNewReplies = ImmutableSet.<PostReply>builder()
496                                 .addAll(newReplyNotification.getElements())
497                                 .addAll(localReplyNotification.getElements())
498                                 .build();
499                 return from(allNewReplies).filter(replyVisibilityFilter.isVisible(currentSone)).toSet();
500         }
501
502         /**
503          * Sets whether the current start of the plugin is the first start. It is
504          * considered a first start if the configuration file does not exist.
505          *
506          * @param firstStart
507          *            {@code true} if no configuration file existed when Sone was
508          *            loaded, {@code false} otherwise
509          */
510         public void setFirstStart(boolean firstStart) {
511                 if (firstStart) {
512                         Template firstStartNotificationTemplate = loaders.loadTemplate("/templates/notify/firstStartNotification.html");
513                         Notification firstStartNotification = new TemplateNotification("first-start-notification", firstStartNotificationTemplate);
514                         notificationManager.addNotification(firstStartNotification);
515                 }
516         }
517
518         /**
519          * Sets whether Sone was started with a fresh configuration file.
520          *
521          * @param newConfig
522          *            {@code true} if Sone was started with a fresh configuration,
523          *            {@code false} if the existing configuration could be read
524          */
525         public void setNewConfig(boolean newConfig) {
526                 if (newConfig && !hasFirstStartNotification()) {
527                         Template configNotReadNotificationTemplate = loaders.loadTemplate("/templates/notify/configNotReadNotification.html");
528                         Notification configNotReadNotification = new TemplateNotification("config-not-read-notification", configNotReadNotificationTemplate);
529                         notificationManager.addNotification(configNotReadNotification);
530                 }
531         }
532
533         //
534         // PRIVATE ACCESSORS
535         //
536
537         /**
538          * Returns whether the first start notification is currently displayed.
539          *
540          * @return {@code true} if the first-start notification is currently
541          *         displayed, {@code false} otherwise
542          */
543         private boolean hasFirstStartNotification() {
544                 return notificationManager.getNotification("first-start-notification") != null;
545         }
546
547         //
548         // ACTIONS
549         //
550
551         /**
552          * Starts the web interface and registers all toadlets.
553          */
554         public void start() {
555                 registerToadlets();
556
557                 /* notification templates. */
558                 Template startupNotificationTemplate = loaders.loadTemplate("/templates/notify/startupNotification.html");
559
560                 final TemplateNotification startupNotification = new TemplateNotification("startup-notification", startupNotificationTemplate);
561                 notificationManager.addNotification(startupNotification);
562
563                 ticker.schedule(new Runnable() {
564
565                         @Override
566                         public void run() {
567                                 startupNotification.dismiss();
568                         }
569                 }, 2, TimeUnit.MINUTES);
570
571                 Template wotMissingNotificationTemplate = loaders.loadTemplate("/templates/notify/wotMissingNotification.html");
572                 final TemplateNotification wotMissingNotification = new TemplateNotification("wot-missing-notification", wotMissingNotificationTemplate);
573                 ticker.scheduleAtFixedRate(new Runnable() {
574
575                         @Override
576                         @SuppressWarnings("synthetic-access")
577                         public void run() {
578                                 if (getCore().getIdentityManager().isConnected()) {
579                                         wotMissingNotification.dismiss();
580                                 } else {
581                                         notificationManager.addNotification(wotMissingNotification);
582                                 }
583                         }
584
585                 }, 15, 15, TimeUnit.SECONDS);
586         }
587
588         /**
589          * Stops the web interface and unregisters all toadlets.
590          */
591         public void stop() {
592                 pageToadletRegistry.unregisterToadlets();
593                 ticker.shutdownNow();
594         }
595
596         //
597         // PRIVATE METHODS
598         //
599
600         /**
601          * Register all toadlets.
602          */
603         private void registerToadlets() {
604                 Template emptyTemplate = parse(new StringReader(""));
605                 Template loginTemplate = loaders.loadTemplate("/templates/login.html");
606                 Template indexTemplate = loaders.loadTemplate("/templates/index.html");
607                 Template newTemplate = loaders.loadTemplate("/templates/new.html");
608                 Template knownSonesTemplate = loaders.loadTemplate("/templates/knownSones.html");
609                 Template createSoneTemplate = loaders.loadTemplate("/templates/createSone.html");
610                 Template createPostTemplate = loaders.loadTemplate("/templates/createPost.html");
611                 Template createReplyTemplate = loaders.loadTemplate("/templates/createReply.html");
612                 Template bookmarksTemplate = loaders.loadTemplate("/templates/bookmarks.html");
613                 Template searchTemplate = loaders.loadTemplate("/templates/search.html");
614                 Template editProfileTemplate = loaders.loadTemplate("/templates/editProfile.html");
615                 Template editProfileFieldTemplate = loaders.loadTemplate("/templates/editProfileField.html");
616                 Template deleteProfileFieldTemplate = loaders.loadTemplate("/templates/deleteProfileField.html");
617                 Template viewSoneTemplate = loaders.loadTemplate("/templates/viewSone.html");
618                 Template viewPostTemplate = loaders.loadTemplate("/templates/viewPost.html");
619                 Template deletePostTemplate = loaders.loadTemplate("/templates/deletePost.html");
620                 Template deleteReplyTemplate = loaders.loadTemplate("/templates/deleteReply.html");
621                 Template deleteSoneTemplate = loaders.loadTemplate("/templates/deleteSone.html");
622                 Template imageBrowserTemplate = loaders.loadTemplate("/templates/imageBrowser.html");
623                 Template createAlbumTemplate = loaders.loadTemplate("/templates/createAlbum.html");
624                 Template deleteAlbumTemplate = loaders.loadTemplate("/templates/deleteAlbum.html");
625                 Template deleteImageTemplate = loaders.loadTemplate("/templates/deleteImage.html");
626                 Template noPermissionTemplate = loaders.loadTemplate("/templates/noPermission.html");
627                 Template emptyImageTitleTemplate = loaders.loadTemplate("/templates/emptyImageTitle.html");
628                 Template emptyAlbumTitleTemplate = loaders.loadTemplate("/templates/emptyAlbumTitle.html");
629                 Template optionsTemplate = loaders.loadTemplate("/templates/options.html");
630                 Template rescueTemplate = loaders.loadTemplate("/templates/rescue.html");
631                 Template aboutTemplate = loaders.loadTemplate("/templates/about.html");
632                 Template invalidTemplate = loaders.loadTemplate("/templates/invalid.html");
633                 Template postTemplate = loaders.loadTemplate("/templates/include/viewPost.html");
634                 Template replyTemplate = loaders.loadTemplate("/templates/include/viewReply.html");
635                 Template openSearchTemplate = loaders.loadTemplate("/templates/xml/OpenSearch.xml");
636
637                 pageToadletRegistry.addPage(new RedirectPage<FreenetRequest>("", "index.html"));
638                 pageToadletRegistry.addPage(new IndexPage(indexTemplate, this, loaders, postVisibilityFilter));
639                 pageToadletRegistry.addPage(new NewPage(newTemplate, this, loaders));
640                 pageToadletRegistry.addPage(new CreateSonePage(createSoneTemplate, this, loaders));
641                 pageToadletRegistry.addPage(new KnownSonesPage(knownSonesTemplate, this, loaders));
642                 pageToadletRegistry.addPage(new EditProfilePage(editProfileTemplate, this, loaders));
643                 pageToadletRegistry.addPage(new EditProfileFieldPage(editProfileFieldTemplate, this, loaders));
644                 pageToadletRegistry.addPage(new DeleteProfileFieldPage(deleteProfileFieldTemplate, this, loaders));
645                 pageToadletRegistry.addPage(new CreatePostPage(createPostTemplate, this, loaders));
646                 pageToadletRegistry.addPage(new CreateReplyPage(createReplyTemplate, this, loaders));
647                 pageToadletRegistry.addPage(new ViewSonePage(viewSoneTemplate, this, loaders));
648                 pageToadletRegistry.addPage(new ViewPostPage(viewPostTemplate, this, loaders));
649                 pageToadletRegistry.addPage(new LikePage(emptyTemplate, this, loaders));
650                 pageToadletRegistry.addPage(new UnlikePage(emptyTemplate, this, loaders));
651                 pageToadletRegistry.addPage(new DeletePostPage(deletePostTemplate, this, loaders));
652                 pageToadletRegistry.addPage(new DeleteReplyPage(deleteReplyTemplate, this, loaders));
653                 pageToadletRegistry.addPage(new LockSonePage(emptyTemplate, this, loaders));
654                 pageToadletRegistry.addPage(new UnlockSonePage(emptyTemplate, this, loaders));
655                 pageToadletRegistry.addPage(new FollowSonePage(emptyTemplate, this, loaders));
656                 pageToadletRegistry.addPage(new UnfollowSonePage(emptyTemplate, this, loaders));
657                 pageToadletRegistry.addPage(new ImageBrowserPage(imageBrowserTemplate, this, loaders));
658                 pageToadletRegistry.addPage(new CreateAlbumPage(createAlbumTemplate, this, loaders));
659                 pageToadletRegistry.addPage(new EditAlbumPage(emptyTemplate, this, loaders));
660                 pageToadletRegistry.addPage(new DeleteAlbumPage(deleteAlbumTemplate, this, loaders));
661                 pageToadletRegistry.addPage(new UploadImagePage(invalidTemplate, this, loaders));
662                 pageToadletRegistry.addPage(new EditImagePage(emptyTemplate, this, loaders));
663                 pageToadletRegistry.addPage(new DeleteImagePage(deleteImageTemplate, this, loaders));
664                 pageToadletRegistry.addPage(new TrustPage(emptyTemplate, this, loaders));
665                 pageToadletRegistry.addPage(new DistrustPage(emptyTemplate, this, loaders));
666                 pageToadletRegistry.addPage(new UntrustPage(emptyTemplate, this, loaders));
667                 pageToadletRegistry.addPage(new MarkAsKnownPage(emptyTemplate, this, loaders));
668                 pageToadletRegistry.addPage(new BookmarkPage(emptyTemplate, this, loaders));
669                 pageToadletRegistry.addPage(new UnbookmarkPage(emptyTemplate, this, loaders));
670                 pageToadletRegistry.addPage(new BookmarksPage(bookmarksTemplate, this, loaders));
671                 pageToadletRegistry.addPage(new SearchPage(searchTemplate, this, loaders));
672                 pageToadletRegistry.addPage(new DeleteSonePage(deleteSoneTemplate, this, loaders));
673                 pageToadletRegistry.addPage(new LoginPage(loginTemplate, this, loaders));
674                 pageToadletRegistry.addPage(new LogoutPage(emptyTemplate, this, loaders));
675                 pageToadletRegistry.addPage(new OptionsPage(optionsTemplate, this, loaders));
676                 pageToadletRegistry.addPage(new RescuePage(rescueTemplate, this, loaders));
677                 pageToadletRegistry.addPage(new AboutPage(aboutTemplate, this, loaders, new PluginVersion(SonePlugin.getPluginVersion()), new PluginYear(sonePlugin.getYear()), new PluginHomepage(sonePlugin.getHomepage())));
678                 pageToadletRegistry.addPage(new SoneTemplatePage("noPermission.html", this, loaders, noPermissionTemplate, "Page.NoPermission.Title"));
679                 pageToadletRegistry.addPage(new SoneTemplatePage("emptyImageTitle.html", this, loaders, emptyImageTitleTemplate, "Page.EmptyImageTitle.Title"));
680                 pageToadletRegistry.addPage(new SoneTemplatePage("emptyAlbumTitle.html", this, loaders, emptyAlbumTitleTemplate, "Page.EmptyAlbumTitle.Title"));
681                 pageToadletRegistry.addPage(new DismissNotificationPage(emptyTemplate, this, loaders));
682                 pageToadletRegistry.addPage(new SoneTemplatePage("invalid.html", this, loaders, invalidTemplate, "Page.Invalid.Title"));
683                 pageToadletRegistry.addPage(loaders.<FreenetRequest>loadStaticPage("css/", "/static/css/", "text/css"));
684                 pageToadletRegistry.addPage(loaders.<FreenetRequest>loadStaticPage("javascript/", "/static/javascript/", "text/javascript"));
685                 pageToadletRegistry.addPage(loaders.<FreenetRequest>loadStaticPage("images/", "/static/images/", "image/png"));
686                 pageToadletRegistry.addPage(new TemplatePage<FreenetRequest>("OpenSearch.xml", "application/opensearchdescription+xml", templateContextFactory, openSearchTemplate));
687                 pageToadletRegistry.addPage(new GetImagePage(this));
688                 pageToadletRegistry.addPage(new GetTranslationAjaxPage(this));
689                 pageToadletRegistry.addPage(new GetStatusAjaxPage(this, elementLoader, timeTextConverter, l10nFilter, TimeZone.getDefault()));
690                 pageToadletRegistry.addPage(new GetNotificationsAjaxPage(this));
691                 pageToadletRegistry.addPage(new DismissNotificationAjaxPage(this));
692                 pageToadletRegistry.addPage(new CreatePostAjaxPage(this));
693                 pageToadletRegistry.addPage(new CreateReplyAjaxPage(this));
694                 pageToadletRegistry.addPage(new GetReplyAjaxPage(this, replyTemplate));
695                 pageToadletRegistry.addPage(new GetPostAjaxPage(this, postTemplate));
696                 pageToadletRegistry.addPage(new GetLinkedElementAjaxPage(this, elementLoader, linkedElementRenderFilter));
697                 pageToadletRegistry.addPage(new GetTimesAjaxPage(this, timeTextConverter, l10nFilter, TimeZone.getDefault()));
698                 pageToadletRegistry.addPage(new MarkAsKnownAjaxPage(this));
699                 pageToadletRegistry.addPage(new DeletePostAjaxPage(this));
700                 pageToadletRegistry.addPage(new DeleteReplyAjaxPage(this));
701                 pageToadletRegistry.addPage(new LockSoneAjaxPage(this));
702                 pageToadletRegistry.addPage(new UnlockSoneAjaxPage(this));
703                 pageToadletRegistry.addPage(new FollowSoneAjaxPage(this));
704                 pageToadletRegistry.addPage(new UnfollowSoneAjaxPage(this));
705                 pageToadletRegistry.addPage(new EditAlbumAjaxPage(this));
706                 pageToadletRegistry.addPage(new EditImageAjaxPage(this, parserFilter, shortenFilter, renderFilter));
707                 pageToadletRegistry.addPage(new TrustAjaxPage(this));
708                 pageToadletRegistry.addPage(new DistrustAjaxPage(this));
709                 pageToadletRegistry.addPage(new UntrustAjaxPage(this));
710                 pageToadletRegistry.addPage(new LikeAjaxPage(this));
711                 pageToadletRegistry.addPage(new UnlikeAjaxPage(this));
712                 pageToadletRegistry.addPage(new GetLikesAjaxPage(this));
713                 pageToadletRegistry.addPage(new BookmarkAjaxPage(this));
714                 pageToadletRegistry.addPage(new UnbookmarkAjaxPage(this));
715                 pageToadletRegistry.addPage(new EditProfileFieldAjaxPage(this));
716                 pageToadletRegistry.addPage(new DeleteProfileFieldAjaxPage(this));
717                 pageToadletRegistry.addPage(new MoveProfileFieldAjaxPage(this));
718
719                 pageToadletRegistry.registerToadlets();
720         }
721
722         /**
723          * Returns all {@link Sone#isLocal() local Sone}s that are referenced by
724          * {@link SonePart}s in the given text (after parsing it using
725          * {@link SoneTextParser}).
726          *
727          * @param text
728          *            The text to parse
729          * @return All mentioned local Sones
730          */
731         private Collection<Sone> getMentionedSones(String text) {
732                 /* we need no context to find mentioned Sones. */
733                 Set<Sone> mentionedSones = new HashSet<>();
734                 for (Part part : soneTextParser.parse(text, null)) {
735                         if (part instanceof SonePart) {
736                                 mentionedSones.add(((SonePart) part).getSone());
737                         }
738                 }
739                 return Collections2.filter(mentionedSones, Sone.LOCAL_SONE_FILTER);
740         }
741
742         /**
743          * Returns the Sone insert notification for the given Sone. If no
744          * notification for the given Sone exists, a new notification is created and
745          * cached.
746          *
747          * @param sone
748          *            The Sone to get the insert notification for
749          * @return The Sone insert notification
750          */
751         private TemplateNotification getSoneInsertNotification(Sone sone) {
752                 synchronized (soneInsertNotifications) {
753                         TemplateNotification templateNotification = soneInsertNotifications.get(sone);
754                         if (templateNotification == null) {
755                                 templateNotification = new TemplateNotification(loaders.loadTemplate("/templates/notify/soneInsertNotification.html"));
756                                 templateNotification.set("insertSone", sone);
757                                 soneInsertNotifications.put(sone, templateNotification);
758                         }
759                         return templateNotification;
760                 }
761         }
762
763         private boolean localSoneMentionedInNewPostOrReply(Post post) {
764                 if (!post.getSone().isLocal()) {
765                         if (!getMentionedSones(post.getText()).isEmpty() && !post.isKnown()) {
766                                 return true;
767                         }
768                 }
769                 for (PostReply postReply : getCore().getReplies(post.getId())) {
770                         if (postReply.getSone().isLocal()) {
771                                 continue;
772                         }
773                         if (!getMentionedSones(postReply.getText()).isEmpty() && !postReply.isKnown()) {
774                                 return true;
775                         }
776                 }
777                 return false;
778         }
779
780         //
781         // EVENT HANDLERS
782         //
783
784         /**
785          * Notifies the web interface that a new {@link Sone} was found.
786          *
787          * @param newSoneFoundEvent
788          *            The event
789          */
790         @Subscribe
791         public void newSoneFound(NewSoneFoundEvent newSoneFoundEvent) {
792                 newSoneNotification.add(newSoneFoundEvent.sone());
793                 if (!hasFirstStartNotification()) {
794                         notificationManager.addNotification(newSoneNotification);
795                 }
796         }
797
798         /**
799          * Notifies the web interface that a new {@link Post} was found.
800          *
801          * @param newPostFoundEvent
802          *            The event
803          */
804         @Subscribe
805         public void newPostFound(NewPostFoundEvent newPostFoundEvent) {
806                 Post post = newPostFoundEvent.post();
807                 boolean isLocal = post.getSone().isLocal();
808                 if (isLocal) {
809                         localPostNotification.add(post);
810                 } else {
811                         newPostNotification.add(post);
812                 }
813                 if (!hasFirstStartNotification()) {
814                         notificationManager.addNotification(isLocal ? localPostNotification : newPostNotification);
815                         if (!getMentionedSones(post.getText()).isEmpty() && !isLocal) {
816                                 mentionNotification.add(post);
817                                 notificationManager.addNotification(mentionNotification);
818                         }
819                 } else {
820                         getCore().markPostKnown(post);
821                 }
822         }
823
824         /**
825          * Notifies the web interface that a new {@link PostReply} was found.
826          *
827          * @param newPostReplyFoundEvent
828          *            The event
829          */
830         @Subscribe
831         public void newReplyFound(NewPostReplyFoundEvent newPostReplyFoundEvent) {
832                 PostReply reply = newPostReplyFoundEvent.postReply();
833                 boolean isLocal = reply.getSone().isLocal();
834                 if (isLocal) {
835                         localReplyNotification.add(reply);
836                 } else {
837                         newReplyNotification.add(reply);
838                 }
839                 if (!hasFirstStartNotification()) {
840                         notificationManager.addNotification(isLocal ? localReplyNotification : newReplyNotification);
841                         if (reply.getPost().isPresent() && localSoneMentionedInNewPostOrReply(reply.getPost().get())) {
842                                 mentionNotification.add(reply.getPost().get());
843                                 notificationManager.addNotification(mentionNotification);
844                         }
845                 } else {
846                         getCore().markReplyKnown(reply);
847                 }
848         }
849
850         /**
851          * Notifies the web interface that a {@link Sone} was marked as known.
852          *
853          * @param markSoneKnownEvent
854          *            The event
855          */
856         @Subscribe
857         public void markSoneKnown(MarkSoneKnownEvent markSoneKnownEvent) {
858                 newSoneNotification.remove(markSoneKnownEvent.sone());
859         }
860
861         @Subscribe
862         public void markPostKnown(MarkPostKnownEvent markPostKnownEvent) {
863                 removePost(markPostKnownEvent.post());
864         }
865
866         @Subscribe
867         public void markReplyKnown(MarkPostReplyKnownEvent markPostReplyKnownEvent) {
868                 removeReply(markPostReplyKnownEvent.postReply());
869         }
870
871         @Subscribe
872         public void soneRemoved(SoneRemovedEvent soneRemovedEvent) {
873                 newSoneNotification.remove(soneRemovedEvent.sone());
874         }
875
876         @Subscribe
877         public void postRemoved(PostRemovedEvent postRemovedEvent) {
878                 removePost(postRemovedEvent.post());
879         }
880
881         private void removePost(Post post) {
882                 newPostNotification.remove(post);
883                 localPostNotification.remove(post);
884                 if (!localSoneMentionedInNewPostOrReply(post)) {
885                         mentionNotification.remove(post);
886                 }
887         }
888
889         @Subscribe
890         public void replyRemoved(PostReplyRemovedEvent postReplyRemovedEvent) {
891                 removeReply(postReplyRemovedEvent.postReply());
892         }
893
894         private void removeReply(PostReply reply) {
895                 newReplyNotification.remove(reply);
896                 localReplyNotification.remove(reply);
897                 if (reply.getPost().isPresent() && !localSoneMentionedInNewPostOrReply(reply.getPost().get())) {
898                         mentionNotification.remove(reply.getPost().get());
899                 }
900         }
901
902         /**
903          * Notifies the web interface that a Sone was locked.
904          *
905          * @param soneLockedEvent
906          *            The event
907          */
908         @Subscribe
909         public void soneLocked(SoneLockedEvent soneLockedEvent) {
910                 final Sone sone = soneLockedEvent.sone();
911                 ScheduledFuture<?> tickerObject = ticker.schedule(new Runnable() {
912
913                         @Override
914                         @SuppressWarnings("synthetic-access")
915                         public void run() {
916                                 lockedSonesNotification.add(sone);
917                                 notificationManager.addNotification(lockedSonesNotification);
918                         }
919                 }, 5, TimeUnit.MINUTES);
920                 lockedSonesTickerObjects.put(sone, tickerObject);
921         }
922
923         /**
924          * Notifies the web interface that a Sone was unlocked.
925          *
926          * @param soneUnlockedEvent
927          *            The event
928          */
929         @Subscribe
930         public void soneUnlocked(SoneUnlockedEvent soneUnlockedEvent) {
931                 lockedSonesNotification.remove(soneUnlockedEvent.sone());
932                 lockedSonesTickerObjects.remove(soneUnlockedEvent.sone()).cancel(false);
933         }
934
935         /**
936          * Notifies the web interface that a {@link Sone} is being inserted.
937          *
938          * @param soneInsertingEvent
939          *            The event
940          */
941         @Subscribe
942         public void soneInserting(SoneInsertingEvent soneInsertingEvent) {
943                 TemplateNotification soneInsertNotification = getSoneInsertNotification(soneInsertingEvent.sone());
944                 soneInsertNotification.set("soneStatus", "inserting");
945                 if (soneInsertingEvent.sone().getOptions().isSoneInsertNotificationEnabled()) {
946                         notificationManager.addNotification(soneInsertNotification);
947                 }
948         }
949
950         /**
951          * Notifies the web interface that a {@link Sone} was inserted.
952          *
953          * @param soneInsertedEvent
954          *            The event
955          */
956         @Subscribe
957         public void soneInserted(SoneInsertedEvent soneInsertedEvent) {
958                 TemplateNotification soneInsertNotification = getSoneInsertNotification(soneInsertedEvent.sone());
959                 soneInsertNotification.set("soneStatus", "inserted");
960                 soneInsertNotification.set("insertDuration", soneInsertedEvent.insertDuration() / 1000);
961                 if (soneInsertedEvent.sone().getOptions().isSoneInsertNotificationEnabled()) {
962                         notificationManager.addNotification(soneInsertNotification);
963                 }
964         }
965
966         /**
967          * Notifies the web interface that a {@link Sone} insert was aborted.
968          *
969          * @param soneInsertAbortedEvent
970          *            The event
971          */
972         @Subscribe
973         public void soneInsertAborted(SoneInsertAbortedEvent soneInsertAbortedEvent) {
974                 TemplateNotification soneInsertNotification = getSoneInsertNotification(soneInsertAbortedEvent.sone());
975                 soneInsertNotification.set("soneStatus", "insert-aborted");
976                 soneInsertNotification.set("insert-error", soneInsertAbortedEvent.cause());
977                 if (soneInsertAbortedEvent.sone().getOptions().isSoneInsertNotificationEnabled()) {
978                         notificationManager.addNotification(soneInsertNotification);
979                 }
980         }
981
982         /**
983          * Notifies the web interface that a new Sone version was found.
984          *
985          * @param updateFoundEvent
986          *            The event
987          */
988         @Subscribe
989         public void updateFound(UpdateFoundEvent updateFoundEvent) {
990                 newVersionNotification.set("latestVersion", updateFoundEvent.version());
991                 newVersionNotification.set("latestEdition", updateFoundEvent.latestEdition());
992                 newVersionNotification.set("releaseTime", updateFoundEvent.releaseTime());
993                 newVersionNotification.set("disruptive", updateFoundEvent.disruptive());
994                 notificationManager.addNotification(newVersionNotification);
995         }
996
997         /**
998          * Notifies the web interface that an image insert was started
999          *
1000          * @param imageInsertStartedEvent
1001          *            The event
1002          */
1003         @Subscribe
1004         public void imageInsertStarted(ImageInsertStartedEvent imageInsertStartedEvent) {
1005                 insertingImagesNotification.add(imageInsertStartedEvent.image());
1006                 notificationManager.addNotification(insertingImagesNotification);
1007         }
1008
1009         /**
1010          * Notifies the web interface that an {@link Image} insert was aborted.
1011          *
1012          * @param imageInsertAbortedEvent
1013          *            The event
1014          */
1015         @Subscribe
1016         public void imageInsertAborted(ImageInsertAbortedEvent imageInsertAbortedEvent) {
1017                 insertingImagesNotification.remove(imageInsertAbortedEvent.image());
1018         }
1019
1020         /**
1021          * Notifies the web interface that an {@link Image} insert is finished.
1022          *
1023          * @param imageInsertFinishedEvent
1024          *            The event
1025          */
1026         @Subscribe
1027         public void imageInsertFinished(ImageInsertFinishedEvent imageInsertFinishedEvent) {
1028                 insertingImagesNotification.remove(imageInsertFinishedEvent.image());
1029                 insertedImagesNotification.add(imageInsertFinishedEvent.image());
1030                 notificationManager.addNotification(insertedImagesNotification);
1031         }
1032
1033         /**
1034          * Notifies the web interface that an {@link Image} insert has failed.
1035          *
1036          * @param imageInsertFailedEvent
1037          *            The event
1038          */
1039         @Subscribe
1040         public void imageInsertFailed(ImageInsertFailedEvent imageInsertFailedEvent) {
1041                 insertingImagesNotification.remove(imageInsertFailedEvent.image());
1042                 imageInsertFailedNotification.add(imageInsertFailedEvent.image());
1043                 notificationManager.addNotification(imageInsertFailedNotification);
1044         }
1045
1046 }