Convert update events to EventBus-based events.
[Sone.git] / src / main / java / net / pterodactylus / sone / core / Core.java
1 /*
2  * Sone - Core.java - Copyright © 2010–2012 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.core;
19
20 import java.net.MalformedURLException;
21 import java.util.ArrayList;
22 import java.util.Collection;
23 import java.util.Collections;
24 import java.util.HashMap;
25 import java.util.HashSet;
26 import java.util.List;
27 import java.util.Map;
28 import java.util.Map.Entry;
29 import java.util.Set;
30 import java.util.concurrent.ExecutorService;
31 import java.util.concurrent.Executors;
32 import java.util.logging.Level;
33 import java.util.logging.Logger;
34
35 import net.pterodactylus.sone.core.Options.DefaultOption;
36 import net.pterodactylus.sone.core.Options.Option;
37 import net.pterodactylus.sone.core.Options.OptionWatcher;
38 import net.pterodactylus.sone.core.event.ImageInsertFinishedEvent;
39 import net.pterodactylus.sone.core.event.MarkPostKnownEvent;
40 import net.pterodactylus.sone.core.event.MarkPostReplyKnownEvent;
41 import net.pterodactylus.sone.core.event.MarkSoneKnownEvent;
42 import net.pterodactylus.sone.core.event.NewPostFoundEvent;
43 import net.pterodactylus.sone.core.event.NewPostReplyFoundEvent;
44 import net.pterodactylus.sone.core.event.NewSoneFoundEvent;
45 import net.pterodactylus.sone.core.event.PostRemovedEvent;
46 import net.pterodactylus.sone.core.event.PostReplyRemovedEvent;
47 import net.pterodactylus.sone.core.event.SoneLockedEvent;
48 import net.pterodactylus.sone.core.event.SoneRemovedEvent;
49 import net.pterodactylus.sone.core.event.SoneUnlockedEvent;
50 import net.pterodactylus.sone.data.Album;
51 import net.pterodactylus.sone.data.Client;
52 import net.pterodactylus.sone.data.Image;
53 import net.pterodactylus.sone.data.Post;
54 import net.pterodactylus.sone.data.PostReply;
55 import net.pterodactylus.sone.data.Profile;
56 import net.pterodactylus.sone.data.Profile.Field;
57 import net.pterodactylus.sone.data.Reply;
58 import net.pterodactylus.sone.data.Sone;
59 import net.pterodactylus.sone.data.Sone.ShowCustomAvatars;
60 import net.pterodactylus.sone.data.Sone.SoneStatus;
61 import net.pterodactylus.sone.data.TemporaryImage;
62 import net.pterodactylus.sone.data.impl.PostImpl;
63 import net.pterodactylus.sone.fcp.FcpInterface;
64 import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired;
65 import net.pterodactylus.sone.freenet.wot.Identity;
66 import net.pterodactylus.sone.freenet.wot.IdentityListener;
67 import net.pterodactylus.sone.freenet.wot.IdentityManager;
68 import net.pterodactylus.sone.freenet.wot.OwnIdentity;
69 import net.pterodactylus.sone.main.SonePlugin;
70 import net.pterodactylus.util.config.Configuration;
71 import net.pterodactylus.util.config.ConfigurationException;
72 import net.pterodactylus.util.logging.Logging;
73 import net.pterodactylus.util.number.Numbers;
74 import net.pterodactylus.util.service.AbstractService;
75 import net.pterodactylus.util.thread.NamedThreadFactory;
76 import net.pterodactylus.util.thread.Ticker;
77 import net.pterodactylus.util.validation.EqualityValidator;
78 import net.pterodactylus.util.validation.IntegerRangeValidator;
79 import net.pterodactylus.util.validation.OrValidator;
80 import net.pterodactylus.util.validation.Validation;
81
82 import com.google.common.base.Predicate;
83 import com.google.common.collect.Collections2;
84 import com.google.common.eventbus.EventBus;
85 import com.google.common.eventbus.Subscribe;
86 import com.google.inject.Inject;
87
88 import freenet.keys.FreenetURI;
89
90 /**
91  * The Sone core.
92  *
93  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
94  */
95 public class Core extends AbstractService implements IdentityListener, SoneProvider, PostProvider {
96
97         /** The logger. */
98         private static final Logger logger = Logging.getLogger(Core.class);
99
100         /** The start time. */
101         private final long startupTime = System.currentTimeMillis();
102
103         /** The options. */
104         private final Options options = new Options();
105
106         /** The preferences. */
107         private final Preferences preferences = new Preferences(options);
108
109         /** The event bus. */
110         private final EventBus eventBus;
111
112         /** The configuration. */
113         private Configuration configuration;
114
115         /** Whether we’re currently saving the configuration. */
116         private boolean storingConfiguration = false;
117
118         /** The identity manager. */
119         private final IdentityManager identityManager;
120
121         /** Interface to freenet. */
122         private final FreenetInterface freenetInterface;
123
124         /** The Sone downloader. */
125         private final SoneDownloader soneDownloader;
126
127         /** The image inserter. */
128         private final ImageInserter imageInserter;
129
130         /** Sone downloader thread-pool. */
131         private final ExecutorService soneDownloaders = Executors.newFixedThreadPool(10, new NamedThreadFactory("Sone Downloader %2$d"));
132
133         /** The update checker. */
134         private final UpdateChecker updateChecker;
135
136         /** The trust updater. */
137         private final WebOfTrustUpdater webOfTrustUpdater;
138
139         /** The FCP interface. */
140         private volatile FcpInterface fcpInterface;
141
142         /** The times Sones were followed. */
143         private final Map<Sone, Long> soneFollowingTimes = new HashMap<Sone, Long>();
144
145         /** Locked local Sones. */
146         /* synchronize on itself. */
147         private final Set<Sone> lockedSones = new HashSet<Sone>();
148
149         /** Sone inserters. */
150         /* synchronize access on this on sones. */
151         private final Map<Sone, SoneInserter> soneInserters = new HashMap<Sone, SoneInserter>();
152
153         /** Sone rescuers. */
154         /* synchronize access on this on sones. */
155         private final Map<Sone, SoneRescuer> soneRescuers = new HashMap<Sone, SoneRescuer>();
156
157         /** All Sones. */
158         /* synchronize access on this on itself. */
159         private final Map<String, Sone> sones = new HashMap<String, Sone>();
160
161         /** All known Sones. */
162         private final Set<String> knownSones = new HashSet<String>();
163
164         /** All posts. */
165         private final Map<String, Post> posts = new HashMap<String, Post>();
166
167         /** All known posts. */
168         private final Set<String> knownPosts = new HashSet<String>();
169
170         /** All replies. */
171         private final Map<String, PostReply> replies = new HashMap<String, PostReply>();
172
173         /** All known replies. */
174         private final Set<String> knownReplies = new HashSet<String>();
175
176         /** All bookmarked posts. */
177         /* synchronize access on itself. */
178         private final Set<String> bookmarkedPosts = new HashSet<String>();
179
180         /** Trusted identities, sorted by own identities. */
181         private final Map<OwnIdentity, Set<Identity>> trustedIdentities = Collections.synchronizedMap(new HashMap<OwnIdentity, Set<Identity>>());
182
183         /** All known albums. */
184         private final Map<String, Album> albums = new HashMap<String, Album>();
185
186         /** All known images. */
187         private final Map<String, Image> images = new HashMap<String, Image>();
188
189         /** All temporary images. */
190         private final Map<String, TemporaryImage> temporaryImages = new HashMap<String, TemporaryImage>();
191
192         /** Ticker for threads that mark own elements as known. */
193         private final Ticker localElementTicker = new Ticker();
194
195         /** The time the configuration was last touched. */
196         private volatile long lastConfigurationUpdate;
197
198         /**
199          * Creates a new core.
200          *
201          * @param configuration
202          *            The configuration of the core
203          * @param freenetInterface
204          *            The freenet interface
205          * @param identityManager
206          *            The identity manager
207          * @param webOfTrustUpdater
208          *            The WebOfTrust updater
209          * @param eventBus
210          *            The event bus
211          */
212         @Inject
213         public Core(Configuration configuration, FreenetInterface freenetInterface, IdentityManager identityManager, WebOfTrustUpdater webOfTrustUpdater, EventBus eventBus) {
214                 super("Sone Core");
215                 this.configuration = configuration;
216                 this.freenetInterface = freenetInterface;
217                 this.identityManager = identityManager;
218                 this.soneDownloader = new SoneDownloader(this, freenetInterface);
219                 this.imageInserter = new ImageInserter(freenetInterface);
220                 this.updateChecker = new UpdateChecker(eventBus, freenetInterface);
221                 this.webOfTrustUpdater = webOfTrustUpdater;
222                 this.eventBus = eventBus;
223         }
224
225         //
226         // ACCESSORS
227         //
228
229         /**
230          * Returns the time Sone was started.
231          *
232          * @return The startup time (in milliseconds since Jan 1, 1970 UTC)
233          */
234         public long getStartupTime() {
235                 return startupTime;
236         }
237
238         /**
239          * Sets the configuration to use. This will automatically save the current
240          * configuration to the given configuration.
241          *
242          * @param configuration
243          *            The new configuration to use
244          */
245         public void setConfiguration(Configuration configuration) {
246                 this.configuration = configuration;
247                 touchConfiguration();
248         }
249
250         /**
251          * Returns the options used by the core.
252          *
253          * @return The options of the core
254          */
255         public Preferences getPreferences() {
256                 return preferences;
257         }
258
259         /**
260          * Returns the identity manager used by the core.
261          *
262          * @return The identity manager
263          */
264         public IdentityManager getIdentityManager() {
265                 return identityManager;
266         }
267
268         /**
269          * Returns the update checker.
270          *
271          * @return The update checker
272          */
273         public UpdateChecker getUpdateChecker() {
274                 return updateChecker;
275         }
276
277         /**
278          * Sets the FCP interface to use.
279          *
280          * @param fcpInterface
281          *            The FCP interface to use
282          */
283         public void setFcpInterface(FcpInterface fcpInterface) {
284                 this.fcpInterface = fcpInterface;
285         }
286
287         /**
288          * Returns the Sone rescuer for the given local Sone.
289          *
290          * @param sone
291          *            The local Sone to get the rescuer for
292          * @return The Sone rescuer for the given Sone
293          */
294         public SoneRescuer getSoneRescuer(Sone sone) {
295                 Validation.begin().isNotNull("Sone", sone).check().is("Local Sone", sone.isLocal()).check();
296                 synchronized (sones) {
297                         SoneRescuer soneRescuer = soneRescuers.get(sone);
298                         if (soneRescuer == null) {
299                                 soneRescuer = new SoneRescuer(this, soneDownloader, sone);
300                                 soneRescuers.put(sone, soneRescuer);
301                                 soneRescuer.start();
302                         }
303                         return soneRescuer;
304                 }
305         }
306
307         /**
308          * Returns whether the given Sone is currently locked.
309          *
310          * @param sone
311          *            The sone to check
312          * @return {@code true} if the Sone is locked, {@code false} if it is not
313          */
314         public boolean isLocked(Sone sone) {
315                 synchronized (lockedSones) {
316                         return lockedSones.contains(sone);
317                 }
318         }
319
320         /**
321          * Returns all Sones, remote and local.
322          *
323          * @return All Sones
324          */
325         public Set<Sone> getSones() {
326                 return new HashSet<Sone>(sones.values());
327         }
328
329         /**
330          * Returns the Sone with the given ID, regardless whether it’s local or
331          * remote.
332          *
333          * @param id
334          *            The ID of the Sone to get
335          * @return The Sone with the given ID, or {@code null} if there is no such
336          *         Sone
337          */
338         public Sone getSone(String id) {
339                 return getSone(id, true);
340         }
341
342         /**
343          * Returns the Sone with the given ID, regardless whether it’s local or
344          * remote.
345          *
346          * @param id
347          *            The ID of the Sone to get
348          * @param create
349          *            {@code true} to create a new Sone if none exists,
350          *            {@code false} to return {@code null} if a Sone with the given
351          *            ID does not exist
352          * @return The Sone with the given ID, or {@code null} if there is no such
353          *         Sone
354          */
355         @Override
356         public Sone getSone(String id, boolean create) {
357                 synchronized (sones) {
358                         if (!sones.containsKey(id) && create) {
359                                 Sone sone = new Sone(id, false);
360                                 sones.put(id, sone);
361                         }
362                         return sones.get(id);
363                 }
364         }
365
366         /**
367          * Checks whether the core knows a Sone with the given ID.
368          *
369          * @param id
370          *            The ID of the Sone
371          * @return {@code true} if there is a Sone with the given ID, {@code false}
372          *         otherwise
373          */
374         public boolean hasSone(String id) {
375                 synchronized (sones) {
376                         return sones.containsKey(id);
377                 }
378         }
379
380         /**
381          * Returns all local Sones.
382          *
383          * @return All local Sones
384          */
385         public Collection<Sone> getLocalSones() {
386                 synchronized (sones) {
387                         return Collections2.filter(sones.values(), new Predicate<Sone>() {
388
389                                 @Override
390                                 public boolean apply(Sone sone) {
391                                         return sone.isLocal();
392                                 }
393                         });
394                 }
395         }
396
397         /**
398          * Returns the local Sone with the given ID, optionally creating a new Sone.
399          *
400          * @param id
401          *            The ID of the Sone
402          * @param create
403          *            {@code true} to create a new Sone if none exists,
404          *            {@code false} to return null if none exists
405          * @return The Sone with the given ID, or {@code null}
406          */
407         public Sone getLocalSone(String id, boolean create) {
408                 synchronized (sones) {
409                         Sone sone = sones.get(id);
410                         if ((sone == null) && create) {
411                                 sone = new Sone(id, true);
412                                 sones.put(id, sone);
413                         }
414                         if ((sone != null) && !sone.isLocal()) {
415                                 sone = new Sone(id, true);
416                                 sones.put(id, sone);
417                         }
418                         return sone;
419                 }
420         }
421
422         /**
423          * Returns all remote Sones.
424          *
425          * @return All remote Sones
426          */
427         public Collection<Sone> getRemoteSones() {
428                 synchronized (sones) {
429                         return Collections2.filter(sones.values(), new Predicate<Sone>() {
430
431                                 @Override
432                                 public boolean apply(Sone sone) {
433                                         return !sone.isLocal();
434                                 }
435                         });
436                 }
437         }
438
439         /**
440          * Returns the remote Sone with the given ID.
441          *
442          * @param id
443          *            The ID of the remote Sone to get
444          * @param create
445          *            {@code true} to always create a Sone, {@code false} to return
446          *            {@code null} if no Sone with the given ID exists
447          * @return The Sone with the given ID
448          */
449         public Sone getRemoteSone(String id, boolean create) {
450                 synchronized (sones) {
451                         Sone sone = sones.get(id);
452                         if ((sone == null) && create && (id != null) && (id.length() == 43)) {
453                                 sone = new Sone(id, false);
454                                 sones.put(id, sone);
455                         }
456                         return sone;
457                 }
458         }
459
460         /**
461          * Returns whether the given Sone has been modified.
462          *
463          * @param sone
464          *            The Sone to check for modifications
465          * @return {@code true} if a modification has been detected in the Sone,
466          *         {@code false} otherwise
467          */
468         public boolean isModifiedSone(Sone sone) {
469                 return (soneInserters.containsKey(sone)) ? soneInserters.get(sone).isModified() : false;
470         }
471
472         /**
473          * Returns the time when the given was first followed by any local Sone.
474          *
475          * @param sone
476          *            The Sone to get the time for
477          * @return The time (in milliseconds since Jan 1, 1970) the Sone has first
478          *         been followed, or {@link Long#MAX_VALUE}
479          */
480         public long getSoneFollowingTime(Sone sone) {
481                 synchronized (soneFollowingTimes) {
482                         if (soneFollowingTimes.containsKey(sone)) {
483                                 return soneFollowingTimes.get(sone);
484                         }
485                         return Long.MAX_VALUE;
486                 }
487         }
488
489         /**
490          * Returns whether the target Sone is trusted by the origin Sone.
491          *
492          * @param origin
493          *            The origin Sone
494          * @param target
495          *            The target Sone
496          * @return {@code true} if the target Sone is trusted by the origin Sone
497          */
498         public boolean isSoneTrusted(Sone origin, Sone target) {
499                 Validation.begin().isNotNull("Origin", origin).isNotNull("Target", target).check().isInstanceOf("Origin’s OwnIdentity", origin.getIdentity(), OwnIdentity.class).check();
500                 return trustedIdentities.containsKey(origin.getIdentity()) && trustedIdentities.get(origin.getIdentity()).contains(target.getIdentity());
501         }
502
503         /**
504          * Returns the post with the given ID.
505          *
506          * @param postId
507          *            The ID of the post to get
508          * @return The post with the given ID, or a new post with the given ID
509          */
510         public Post getPost(String postId) {
511                 return getPost(postId, true);
512         }
513
514         /**
515          * Returns the post with the given ID, optionally creating a new post.
516          *
517          * @param postId
518          *            The ID of the post to get
519          * @param create
520          *            {@code true} it create a new post if no post with the given ID
521          *            exists, {@code false} to return {@code null}
522          * @return The post, or {@code null} if there is no such post
523          */
524         @Override
525         public Post getPost(String postId, boolean create) {
526                 synchronized (posts) {
527                         Post post = posts.get(postId);
528                         if ((post == null) && create) {
529                                 post = new PostImpl(postId);
530                                 posts.put(postId, post);
531                         }
532                         return post;
533                 }
534         }
535
536         /**
537          * Returns all posts that have the given Sone as recipient.
538          *
539          * @see Post#getRecipient()
540          * @param recipient
541          *            The recipient of the posts
542          * @return All posts that have the given Sone as recipient
543          */
544         public Set<Post> getDirectedPosts(Sone recipient) {
545                 Validation.begin().isNotNull("Recipient", recipient).check();
546                 Set<Post> directedPosts = new HashSet<Post>();
547                 synchronized (posts) {
548                         for (Post post : posts.values()) {
549                                 if (recipient.equals(post.getRecipient())) {
550                                         directedPosts.add(post);
551                                 }
552                         }
553                 }
554                 return directedPosts;
555         }
556
557         /**
558          * Returns the reply with the given ID. If there is no reply with the given
559          * ID yet, a new one is created, unless {@code create} is false in which
560          * case {@code null} is returned.
561          *
562          * @param replyId
563          *            The ID of the reply to get
564          * @param create
565          *            {@code true} to always return a {@link Reply}, {@code false}
566          *            to return {@code null} if no reply can be found
567          * @return The reply, or {@code null} if there is no such reply
568          */
569         public PostReply getPostReply(String replyId, boolean create) {
570                 synchronized (replies) {
571                         PostReply reply = replies.get(replyId);
572                         if (create && (reply == null)) {
573                                 reply = new PostReply(replyId);
574                                 replies.put(replyId, reply);
575                         }
576                         return reply;
577                 }
578         }
579
580         /**
581          * Returns all replies for the given post, order ascending by time.
582          *
583          * @param post
584          *            The post to get all replies for
585          * @return All replies for the given post
586          */
587         public List<PostReply> getReplies(Post post) {
588                 Set<Sone> sones = getSones();
589                 List<PostReply> replies = new ArrayList<PostReply>();
590                 for (Sone sone : sones) {
591                         for (PostReply reply : sone.getReplies()) {
592                                 if (reply.getPost().equals(post)) {
593                                         replies.add(reply);
594                                 }
595                         }
596                 }
597                 Collections.sort(replies, Reply.TIME_COMPARATOR);
598                 return replies;
599         }
600
601         /**
602          * Returns all Sones that have liked the given post.
603          *
604          * @param post
605          *            The post to get the liking Sones for
606          * @return The Sones that like the given post
607          */
608         public Set<Sone> getLikes(Post post) {
609                 Set<Sone> sones = new HashSet<Sone>();
610                 for (Sone sone : getSones()) {
611                         if (sone.getLikedPostIds().contains(post.getId())) {
612                                 sones.add(sone);
613                         }
614                 }
615                 return sones;
616         }
617
618         /**
619          * Returns all Sones that have liked the given reply.
620          *
621          * @param reply
622          *            The reply to get the liking Sones for
623          * @return The Sones that like the given reply
624          */
625         public Set<Sone> getLikes(PostReply reply) {
626                 Set<Sone> sones = new HashSet<Sone>();
627                 for (Sone sone : getSones()) {
628                         if (sone.getLikedReplyIds().contains(reply.getId())) {
629                                 sones.add(sone);
630                         }
631                 }
632                 return sones;
633         }
634
635         /**
636          * Returns whether the given post is bookmarked.
637          *
638          * @param post
639          *            The post to check
640          * @return {@code true} if the given post is bookmarked, {@code false}
641          *         otherwise
642          */
643         public boolean isBookmarked(Post post) {
644                 return isPostBookmarked(post.getId());
645         }
646
647         /**
648          * Returns whether the post with the given ID is bookmarked.
649          *
650          * @param id
651          *            The ID of the post to check
652          * @return {@code true} if the post with the given ID is bookmarked,
653          *         {@code false} otherwise
654          */
655         public boolean isPostBookmarked(String id) {
656                 synchronized (bookmarkedPosts) {
657                         return bookmarkedPosts.contains(id);
658                 }
659         }
660
661         /**
662          * Returns all currently known bookmarked posts.
663          *
664          * @return All bookmarked posts
665          */
666         public Set<Post> getBookmarkedPosts() {
667                 Set<Post> posts = new HashSet<Post>();
668                 synchronized (bookmarkedPosts) {
669                         for (String bookmarkedPostId : bookmarkedPosts) {
670                                 Post post = getPost(bookmarkedPostId, false);
671                                 if (post != null) {
672                                         posts.add(post);
673                                 }
674                         }
675                 }
676                 return posts;
677         }
678
679         /**
680          * Returns the album with the given ID, creating a new album if no album
681          * with the given ID can be found.
682          *
683          * @param albumId
684          *            The ID of the album
685          * @return The album with the given ID
686          */
687         public Album getAlbum(String albumId) {
688                 return getAlbum(albumId, true);
689         }
690
691         /**
692          * Returns the album with the given ID, optionally creating a new album if
693          * an album with the given ID can not be found.
694          *
695          * @param albumId
696          *            The ID of the album
697          * @param create
698          *            {@code true} to create a new album if none exists for the
699          *            given ID
700          * @return The album with the given ID, or {@code null} if no album with the
701          *         given ID exists and {@code create} is {@code false}
702          */
703         public Album getAlbum(String albumId, boolean create) {
704                 synchronized (albums) {
705                         Album album = albums.get(albumId);
706                         if (create && (album == null)) {
707                                 album = new Album(albumId);
708                                 albums.put(albumId, album);
709                         }
710                         return album;
711                 }
712         }
713
714         /**
715          * Returns the image with the given ID, creating it if necessary.
716          *
717          * @param imageId
718          *            The ID of the image
719          * @return The image with the given ID
720          */
721         public Image getImage(String imageId) {
722                 return getImage(imageId, true);
723         }
724
725         /**
726          * Returns the image with the given ID, optionally creating it if it does
727          * not exist.
728          *
729          * @param imageId
730          *            The ID of the image
731          * @param create
732          *            {@code true} to create an image if none exists with the given
733          *            ID
734          * @return The image with the given ID, or {@code null} if none exists and
735          *         none was created
736          */
737         public Image getImage(String imageId, boolean create) {
738                 synchronized (images) {
739                         Image image = images.get(imageId);
740                         if (create && (image == null)) {
741                                 image = new Image(imageId);
742                                 images.put(imageId, image);
743                         }
744                         return image;
745                 }
746         }
747
748         /**
749          * Returns the temporary image with the given ID.
750          *
751          * @param imageId
752          *            The ID of the temporary image
753          * @return The temporary image, or {@code null} if there is no temporary
754          *         image with the given ID
755          */
756         public TemporaryImage getTemporaryImage(String imageId) {
757                 synchronized (temporaryImages) {
758                         return temporaryImages.get(imageId);
759                 }
760         }
761
762         //
763         // ACTIONS
764         //
765
766         /**
767          * Locks the given Sone. A locked Sone will not be inserted by
768          * {@link SoneInserter} until it is {@link #unlockSone(Sone) unlocked}
769          * again.
770          *
771          * @param sone
772          *            The sone to lock
773          */
774         public void lockSone(Sone sone) {
775                 synchronized (lockedSones) {
776                         if (lockedSones.add(sone)) {
777                                 eventBus.post(new SoneLockedEvent(sone));
778                         }
779                 }
780         }
781
782         /**
783          * Unlocks the given Sone.
784          *
785          * @see #lockSone(Sone)
786          * @param sone
787          *            The sone to unlock
788          */
789         public void unlockSone(Sone sone) {
790                 synchronized (lockedSones) {
791                         if (lockedSones.remove(sone)) {
792                                 eventBus.post(new SoneUnlockedEvent(sone));
793                         }
794                 }
795         }
796
797         /**
798          * Adds a local Sone from the given own identity.
799          *
800          * @param ownIdentity
801          *            The own identity to create a Sone from
802          * @return The added (or already existing) Sone
803          */
804         public Sone addLocalSone(OwnIdentity ownIdentity) {
805                 if (ownIdentity == null) {
806                         logger.log(Level.WARNING, "Given OwnIdentity is null!");
807                         return null;
808                 }
809                 synchronized (sones) {
810                         final Sone sone;
811                         try {
812                                 sone = getLocalSone(ownIdentity.getId(), true).setIdentity(ownIdentity).setInsertUri(new FreenetURI(ownIdentity.getInsertUri())).setRequestUri(new FreenetURI(ownIdentity.getRequestUri()));
813                         } catch (MalformedURLException mue1) {
814                                 logger.log(Level.SEVERE, String.format("Could not convert the Identity’s URIs to Freenet URIs: %s, %s", ownIdentity.getInsertUri(), ownIdentity.getRequestUri()), mue1);
815                                 return null;
816                         }
817                         sone.setLatestEdition(Numbers.safeParseLong(ownIdentity.getProperty("Sone.LatestEdition"), (long) 0));
818                         sone.setClient(new Client("Sone", SonePlugin.VERSION.toString()));
819                         sone.setKnown(true);
820                         /* TODO - load posts ’n stuff */
821                         sones.put(ownIdentity.getId(), sone);
822                         final SoneInserter soneInserter = new SoneInserter(this, eventBus, freenetInterface, sone);
823                         soneInserters.put(sone, soneInserter);
824                         sone.setStatus(SoneStatus.idle);
825                         loadSone(sone);
826                         soneInserter.start();
827                         return sone;
828                 }
829         }
830
831         /**
832          * Creates a new Sone for the given own identity.
833          *
834          * @param ownIdentity
835          *            The own identity to create a Sone for
836          * @return The created Sone
837          */
838         public Sone createSone(OwnIdentity ownIdentity) {
839                 if (!webOfTrustUpdater.addContextWait(ownIdentity, "Sone")) {
840                         logger.log(Level.SEVERE, String.format("Could not add “Sone” context to own identity: %s", ownIdentity));
841                         return null;
842                 }
843                 Sone sone = addLocalSone(ownIdentity);
844                 sone.getOptions().addBooleanOption("AutoFollow", new DefaultOption<Boolean>(false));
845                 sone.getOptions().addBooleanOption("EnableSoneInsertNotifications", new DefaultOption<Boolean>(false));
846                 sone.getOptions().addBooleanOption("ShowNotification/NewSones", new DefaultOption<Boolean>(true));
847                 sone.getOptions().addBooleanOption("ShowNotification/NewPosts", new DefaultOption<Boolean>(true));
848                 sone.getOptions().addBooleanOption("ShowNotification/NewReplies", new DefaultOption<Boolean>(true));
849                 sone.getOptions().addEnumOption("ShowCustomAvatars", new DefaultOption<ShowCustomAvatars>(ShowCustomAvatars.NEVER));
850
851                 followSone(sone, getSone("nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI"));
852                 touchConfiguration();
853                 return sone;
854         }
855
856         /**
857          * Adds the Sone of the given identity.
858          *
859          * @param identity
860          *            The identity whose Sone to add
861          * @return The added or already existing Sone
862          */
863         public Sone addRemoteSone(Identity identity) {
864                 if (identity == null) {
865                         logger.log(Level.WARNING, "Given Identity is null!");
866                         return null;
867                 }
868                 synchronized (sones) {
869                         final Sone sone = getRemoteSone(identity.getId(), true).setIdentity(identity);
870                         boolean newSone = sone.getRequestUri() == null;
871                         sone.setRequestUri(getSoneUri(identity.getRequestUri()));
872                         sone.setLatestEdition(Numbers.safeParseLong(identity.getProperty("Sone.LatestEdition"), (long) 0));
873                         if (newSone) {
874                                 synchronized (knownSones) {
875                                         newSone = !knownSones.contains(sone.getId());
876                                 }
877                                 sone.setKnown(!newSone);
878                                 if (newSone) {
879                                         eventBus.post(new NewSoneFoundEvent(sone));
880                                         for (Sone localSone : getLocalSones()) {
881                                                 if (localSone.getOptions().getBooleanOption("AutoFollow").get()) {
882                                                         followSone(localSone, sone);
883                                                 }
884                                         }
885                                 }
886                         }
887                         soneDownloader.addSone(sone);
888                         soneDownloaders.execute(new Runnable() {
889
890                                 @Override
891                                 @SuppressWarnings("synthetic-access")
892                                 public void run() {
893                                         soneDownloader.fetchSone(sone, sone.getRequestUri());
894                                 }
895
896                         });
897                         return sone;
898                 }
899         }
900
901         /**
902          * Lets the given local Sone follow the Sone with the given ID.
903          *
904          * @param sone
905          *            The local Sone that should follow another Sone
906          * @param soneId
907          *            The ID of the Sone to follow
908          */
909         public void followSone(Sone sone, String soneId) {
910                 Validation.begin().isNotNull("Sone", sone).isNotNull("Sone ID", soneId).check();
911                 Sone followedSone = getSone(soneId, true);
912                 if (followedSone == null) {
913                         logger.log(Level.INFO, String.format("Ignored Sone with invalid ID: %s", soneId));
914                         return;
915                 }
916                 followSone(sone, getSone(soneId));
917         }
918
919         /**
920          * Lets the given local Sone follow the other given Sone. If the given Sone
921          * was not followed by any local Sone before, this will mark all elements of
922          * the followed Sone as read that have been created before the current
923          * moment.
924          *
925          * @param sone
926          *            The local Sone that should follow the other Sone
927          * @param followedSone
928          *            The Sone that should be followed
929          */
930         public void followSone(Sone sone, Sone followedSone) {
931                 Validation.begin().isNotNull("Sone", sone).isNotNull("Followed Sone", followedSone).check();
932                 sone.addFriend(followedSone.getId());
933                 synchronized (soneFollowingTimes) {
934                         if (!soneFollowingTimes.containsKey(followedSone)) {
935                                 long now = System.currentTimeMillis();
936                                 soneFollowingTimes.put(followedSone, now);
937                                 for (Post post : followedSone.getPosts()) {
938                                         if (post.getTime() < now) {
939                                                 markPostKnown(post);
940                                         }
941                                 }
942                                 for (PostReply reply : followedSone.getReplies()) {
943                                         if (reply.getTime() < now) {
944                                                 markReplyKnown(reply);
945                                         }
946                                 }
947                         }
948                 }
949                 touchConfiguration();
950         }
951
952         /**
953          * Lets the given local Sone unfollow the Sone with the given ID.
954          *
955          * @param sone
956          *            The local Sone that should unfollow another Sone
957          * @param soneId
958          *            The ID of the Sone being unfollowed
959          */
960         public void unfollowSone(Sone sone, String soneId) {
961                 Validation.begin().isNotNull("Sone", sone).isNotNull("Sone ID", soneId).check();
962                 unfollowSone(sone, getSone(soneId, false));
963         }
964
965         /**
966          * Lets the given local Sone unfollow the other given Sone. If the given
967          * local Sone is the last local Sone that followed the given Sone, its
968          * following time will be removed.
969          *
970          * @param sone
971          *            The local Sone that should unfollow another Sone
972          * @param unfollowedSone
973          *            The Sone being unfollowed
974          */
975         public void unfollowSone(Sone sone, Sone unfollowedSone) {
976                 Validation.begin().isNotNull("Sone", sone).isNotNull("Unfollowed Sone", unfollowedSone).check();
977                 sone.removeFriend(unfollowedSone.getId());
978                 boolean unfollowedSoneStillFollowed = false;
979                 for (Sone localSone : getLocalSones()) {
980                         unfollowedSoneStillFollowed |= localSone.hasFriend(unfollowedSone.getId());
981                 }
982                 if (!unfollowedSoneStillFollowed) {
983                         synchronized (soneFollowingTimes) {
984                                 soneFollowingTimes.remove(unfollowedSone);
985                         }
986                 }
987                 touchConfiguration();
988         }
989
990         /**
991          * Sets the trust value of the given origin Sone for the target Sone.
992          *
993          * @param origin
994          *            The origin Sone
995          * @param target
996          *            The target Sone
997          * @param trustValue
998          *            The trust value (from {@code -100} to {@code 100})
999          */
1000         public void setTrust(Sone origin, Sone target, int trustValue) {
1001                 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();
1002                 webOfTrustUpdater.setTrust((OwnIdentity) origin.getIdentity(), target.getIdentity(), trustValue, preferences.getTrustComment());
1003         }
1004
1005         /**
1006          * Removes any trust assignment for the given target Sone.
1007          *
1008          * @param origin
1009          *            The trust origin
1010          * @param target
1011          *            The trust target
1012          */
1013         public void removeTrust(Sone origin, Sone target) {
1014                 Validation.begin().isNotNull("Trust Origin", origin).isNotNull("Trust Target", target).check().isInstanceOf("Trust Origin Identity", origin.getIdentity(), OwnIdentity.class).check();
1015                 webOfTrustUpdater.setTrust((OwnIdentity) origin.getIdentity(), target.getIdentity(), null, null);
1016         }
1017
1018         /**
1019          * Assigns the configured positive trust value for the given target.
1020          *
1021          * @param origin
1022          *            The trust origin
1023          * @param target
1024          *            The trust target
1025          */
1026         public void trustSone(Sone origin, Sone target) {
1027                 setTrust(origin, target, preferences.getPositiveTrust());
1028         }
1029
1030         /**
1031          * Assigns the configured negative trust value for the given target.
1032          *
1033          * @param origin
1034          *            The trust origin
1035          * @param target
1036          *            The trust target
1037          */
1038         public void distrustSone(Sone origin, Sone target) {
1039                 setTrust(origin, target, preferences.getNegativeTrust());
1040         }
1041
1042         /**
1043          * Removes the trust assignment for the given target.
1044          *
1045          * @param origin
1046          *            The trust origin
1047          * @param target
1048          *            The trust target
1049          */
1050         public void untrustSone(Sone origin, Sone target) {
1051                 removeTrust(origin, target);
1052         }
1053
1054         /**
1055          * Updates the stored Sone with the given Sone.
1056          *
1057          * @param sone
1058          *            The updated Sone
1059          */
1060         public void updateSone(Sone sone) {
1061                 updateSone(sone, false);
1062         }
1063
1064         /**
1065          * Updates the stored Sone with the given Sone. If {@code soneRescueMode} is
1066          * {@code true}, an older Sone than the current Sone can be given to restore
1067          * an old state.
1068          *
1069          * @param sone
1070          *            The Sone to update
1071          * @param soneRescueMode
1072          *            {@code true} if the stored Sone should be updated regardless
1073          *            of the age of the given Sone
1074          */
1075         public void updateSone(Sone sone, boolean soneRescueMode) {
1076                 if (hasSone(sone.getId())) {
1077                         Sone storedSone = getSone(sone.getId());
1078                         if (!soneRescueMode && !(sone.getTime() > storedSone.getTime())) {
1079                                 logger.log(Level.FINE, String.format("Downloaded Sone %s is not newer than stored Sone %s.", sone, storedSone));
1080                                 return;
1081                         }
1082                         synchronized (posts) {
1083                                 if (!soneRescueMode) {
1084                                         for (Post post : storedSone.getPosts()) {
1085                                                 posts.remove(post.getId());
1086                                                 if (!sone.getPosts().contains(post)) {
1087                                                         eventBus.post(new PostRemovedEvent(post));
1088                                                 }
1089                                         }
1090                                 }
1091                                 List<Post> storedPosts = storedSone.getPosts();
1092                                 synchronized (knownPosts) {
1093                                         for (Post post : sone.getPosts()) {
1094                                                 post.setSone(storedSone).setKnown(knownPosts.contains(post.getId()));
1095                                                 if (!storedPosts.contains(post)) {
1096                                                         if (post.getTime() < getSoneFollowingTime(sone)) {
1097                                                                 knownPosts.add(post.getId());
1098                                                                 post.setKnown(true);
1099                                                         } else if (!knownPosts.contains(post.getId())) {
1100                                                                 eventBus.post(new NewPostFoundEvent(post));
1101                                                         }
1102                                                 }
1103                                                 posts.put(post.getId(), post);
1104                                         }
1105                                 }
1106                         }
1107                         synchronized (replies) {
1108                                 if (!soneRescueMode) {
1109                                         for (PostReply reply : storedSone.getReplies()) {
1110                                                 replies.remove(reply.getId());
1111                                                 if (!sone.getReplies().contains(reply)) {
1112                                                         eventBus.post(new PostReplyRemovedEvent(reply));
1113                                                 }
1114                                         }
1115                                 }
1116                                 Set<PostReply> storedReplies = storedSone.getReplies();
1117                                 synchronized (knownReplies) {
1118                                         for (PostReply reply : sone.getReplies()) {
1119                                                 reply.setSone(storedSone).setKnown(knownReplies.contains(reply.getId()));
1120                                                 if (!storedReplies.contains(reply)) {
1121                                                         if (reply.getTime() < getSoneFollowingTime(sone)) {
1122                                                                 knownReplies.add(reply.getId());
1123                                                                 reply.setKnown(true);
1124                                                         } else if (!knownReplies.contains(reply.getId())) {
1125                                                                 eventBus.post(new NewPostReplyFoundEvent(reply));
1126                                                         }
1127                                                 }
1128                                                 replies.put(reply.getId(), reply);
1129                                         }
1130                                 }
1131                         }
1132                         synchronized (albums) {
1133                                 synchronized (images) {
1134                                         for (Album album : storedSone.getAlbums()) {
1135                                                 albums.remove(album.getId());
1136                                                 for (Image image : album.getImages()) {
1137                                                         images.remove(image.getId());
1138                                                 }
1139                                         }
1140                                         for (Album album : sone.getAlbums()) {
1141                                                 albums.put(album.getId(), album);
1142                                                 for (Image image : album.getImages()) {
1143                                                         images.put(image.getId(), image);
1144                                                 }
1145                                         }
1146                                 }
1147                         }
1148                         synchronized (storedSone) {
1149                                 if (!soneRescueMode || (sone.getTime() > storedSone.getTime())) {
1150                                         storedSone.setTime(sone.getTime());
1151                                 }
1152                                 storedSone.setClient(sone.getClient());
1153                                 storedSone.setProfile(sone.getProfile());
1154                                 if (soneRescueMode) {
1155                                         for (Post post : sone.getPosts()) {
1156                                                 storedSone.addPost(post);
1157                                         }
1158                                         for (PostReply reply : sone.getReplies()) {
1159                                                 storedSone.addReply(reply);
1160                                         }
1161                                         for (String likedPostId : sone.getLikedPostIds()) {
1162                                                 storedSone.addLikedPostId(likedPostId);
1163                                         }
1164                                         for (String likedReplyId : sone.getLikedReplyIds()) {
1165                                                 storedSone.addLikedReplyId(likedReplyId);
1166                                         }
1167                                         for (Album album : sone.getAlbums()) {
1168                                                 storedSone.addAlbum(album);
1169                                         }
1170                                 } else {
1171                                         storedSone.setPosts(sone.getPosts());
1172                                         storedSone.setReplies(sone.getReplies());
1173                                         storedSone.setLikePostIds(sone.getLikedPostIds());
1174                                         storedSone.setLikeReplyIds(sone.getLikedReplyIds());
1175                                         storedSone.setAlbums(sone.getAlbums());
1176                                 }
1177                                 storedSone.setLatestEdition(sone.getLatestEdition());
1178                         }
1179                 }
1180         }
1181
1182         /**
1183          * Deletes the given Sone. This will remove the Sone from the
1184          * {@link #getLocalSones() local Sones}, stop its {@link SoneInserter} and
1185          * remove the context from its identity.
1186          *
1187          * @param sone
1188          *            The Sone to delete
1189          */
1190         public void deleteSone(Sone sone) {
1191                 if (!(sone.getIdentity() instanceof OwnIdentity)) {
1192                         logger.log(Level.WARNING, String.format("Tried to delete Sone of non-own identity: %s", sone));
1193                         return;
1194                 }
1195                 synchronized (sones) {
1196                         if (!getLocalSones().contains(sone)) {
1197                                 logger.log(Level.WARNING, String.format("Tried to delete non-local Sone: %s", sone));
1198                                 return;
1199                         }
1200                         sones.remove(sone.getId());
1201                         SoneInserter soneInserter = soneInserters.remove(sone);
1202                         soneInserter.stop();
1203                 }
1204                 webOfTrustUpdater.removeContext((OwnIdentity) sone.getIdentity(), "Sone");
1205                 webOfTrustUpdater.removeProperty((OwnIdentity) sone.getIdentity(), "Sone.LatestEdition");
1206                 try {
1207                         configuration.getLongValue("Sone/" + sone.getId() + "/Time").setValue(null);
1208                 } catch (ConfigurationException ce1) {
1209                         logger.log(Level.WARNING, "Could not remove Sone from configuration!", ce1);
1210                 }
1211         }
1212
1213         /**
1214          * Marks the given Sone as known. If the Sone was not {@link Post#isKnown()
1215          * known} before, a {@link MarkSoneKnownEvent} is fired.
1216          *
1217          * @param sone
1218          *            The Sone to mark as known
1219          */
1220         public void markSoneKnown(Sone sone) {
1221                 if (!sone.isKnown()) {
1222                         sone.setKnown(true);
1223                         synchronized (knownSones) {
1224                                 knownSones.add(sone.getId());
1225                         }
1226                         eventBus.post(new MarkSoneKnownEvent(sone));
1227                         touchConfiguration();
1228                 }
1229         }
1230
1231         /**
1232          * Loads and updates the given Sone from the configuration. If any error is
1233          * encountered, loading is aborted and the given Sone is not changed.
1234          *
1235          * @param sone
1236          *            The Sone to load and update
1237          */
1238         public void loadSone(Sone sone) {
1239                 if (!sone.isLocal()) {
1240                         logger.log(Level.FINE, String.format("Tried to load non-local Sone: %s", sone));
1241                         return;
1242                 }
1243
1244                 /* initialize options. */
1245                 sone.getOptions().addBooleanOption("AutoFollow", new DefaultOption<Boolean>(false));
1246                 sone.getOptions().addBooleanOption("EnableSoneInsertNotifications", new DefaultOption<Boolean>(false));
1247                 sone.getOptions().addBooleanOption("ShowNotification/NewSones", new DefaultOption<Boolean>(true));
1248                 sone.getOptions().addBooleanOption("ShowNotification/NewPosts", new DefaultOption<Boolean>(true));
1249                 sone.getOptions().addBooleanOption("ShowNotification/NewReplies", new DefaultOption<Boolean>(true));
1250                 sone.getOptions().addEnumOption("ShowCustomAvatars", new DefaultOption<ShowCustomAvatars>(ShowCustomAvatars.NEVER));
1251
1252                 /* load Sone. */
1253                 String sonePrefix = "Sone/" + sone.getId();
1254                 Long soneTime = configuration.getLongValue(sonePrefix + "/Time").getValue(null);
1255                 if (soneTime == null) {
1256                         logger.log(Level.INFO, "Could not load Sone because no Sone has been saved.");
1257                         return;
1258                 }
1259                 String lastInsertFingerprint = configuration.getStringValue(sonePrefix + "/LastInsertFingerprint").getValue("");
1260
1261                 /* load profile. */
1262                 Profile profile = new Profile(sone);
1263                 profile.setFirstName(configuration.getStringValue(sonePrefix + "/Profile/FirstName").getValue(null));
1264                 profile.setMiddleName(configuration.getStringValue(sonePrefix + "/Profile/MiddleName").getValue(null));
1265                 profile.setLastName(configuration.getStringValue(sonePrefix + "/Profile/LastName").getValue(null));
1266                 profile.setBirthDay(configuration.getIntValue(sonePrefix + "/Profile/BirthDay").getValue(null));
1267                 profile.setBirthMonth(configuration.getIntValue(sonePrefix + "/Profile/BirthMonth").getValue(null));
1268                 profile.setBirthYear(configuration.getIntValue(sonePrefix + "/Profile/BirthYear").getValue(null));
1269
1270                 /* load profile fields. */
1271                 while (true) {
1272                         String fieldPrefix = sonePrefix + "/Profile/Fields/" + profile.getFields().size();
1273                         String fieldName = configuration.getStringValue(fieldPrefix + "/Name").getValue(null);
1274                         if (fieldName == null) {
1275                                 break;
1276                         }
1277                         String fieldValue = configuration.getStringValue(fieldPrefix + "/Value").getValue("");
1278                         profile.addField(fieldName).setValue(fieldValue);
1279                 }
1280
1281                 /* load posts. */
1282                 Set<Post> posts = new HashSet<Post>();
1283                 while (true) {
1284                         String postPrefix = sonePrefix + "/Posts/" + posts.size();
1285                         String postId = configuration.getStringValue(postPrefix + "/ID").getValue(null);
1286                         if (postId == null) {
1287                                 break;
1288                         }
1289                         String postRecipientId = configuration.getStringValue(postPrefix + "/Recipient").getValue(null);
1290                         long postTime = configuration.getLongValue(postPrefix + "/Time").getValue((long) 0);
1291                         String postText = configuration.getStringValue(postPrefix + "/Text").getValue(null);
1292                         if ((postTime == 0) || (postText == null)) {
1293                                 logger.log(Level.WARNING, "Invalid post found, aborting load!");
1294                                 return;
1295                         }
1296                         Post post = getPost(postId).setSone(sone).setTime(postTime).setText(postText);
1297                         if ((postRecipientId != null) && (postRecipientId.length() == 43)) {
1298                                 post.setRecipient(getSone(postRecipientId));
1299                         }
1300                         posts.add(post);
1301                 }
1302
1303                 /* load replies. */
1304                 Set<PostReply> replies = new HashSet<PostReply>();
1305                 while (true) {
1306                         String replyPrefix = sonePrefix + "/Replies/" + replies.size();
1307                         String replyId = configuration.getStringValue(replyPrefix + "/ID").getValue(null);
1308                         if (replyId == null) {
1309                                 break;
1310                         }
1311                         String postId = configuration.getStringValue(replyPrefix + "/Post/ID").getValue(null);
1312                         long replyTime = configuration.getLongValue(replyPrefix + "/Time").getValue((long) 0);
1313                         String replyText = configuration.getStringValue(replyPrefix + "/Text").getValue(null);
1314                         if ((postId == null) || (replyTime == 0) || (replyText == null)) {
1315                                 logger.log(Level.WARNING, "Invalid reply found, aborting load!");
1316                                 return;
1317                         }
1318                         replies.add(getPostReply(replyId, true).setSone(sone).setPost(getPost(postId)).setTime(replyTime).setText(replyText));
1319                 }
1320
1321                 /* load post likes. */
1322                 Set<String> likedPostIds = new HashSet<String>();
1323                 while (true) {
1324                         String likedPostId = configuration.getStringValue(sonePrefix + "/Likes/Post/" + likedPostIds.size() + "/ID").getValue(null);
1325                         if (likedPostId == null) {
1326                                 break;
1327                         }
1328                         likedPostIds.add(likedPostId);
1329                 }
1330
1331                 /* load reply likes. */
1332                 Set<String> likedReplyIds = new HashSet<String>();
1333                 while (true) {
1334                         String likedReplyId = configuration.getStringValue(sonePrefix + "/Likes/Reply/" + likedReplyIds.size() + "/ID").getValue(null);
1335                         if (likedReplyId == null) {
1336                                 break;
1337                         }
1338                         likedReplyIds.add(likedReplyId);
1339                 }
1340
1341                 /* load friends. */
1342                 Set<String> friends = new HashSet<String>();
1343                 while (true) {
1344                         String friendId = configuration.getStringValue(sonePrefix + "/Friends/" + friends.size() + "/ID").getValue(null);
1345                         if (friendId == null) {
1346                                 break;
1347                         }
1348                         friends.add(friendId);
1349                 }
1350
1351                 /* load albums. */
1352                 List<Album> topLevelAlbums = new ArrayList<Album>();
1353                 int albumCounter = 0;
1354                 while (true) {
1355                         String albumPrefix = sonePrefix + "/Albums/" + albumCounter++;
1356                         String albumId = configuration.getStringValue(albumPrefix + "/ID").getValue(null);
1357                         if (albumId == null) {
1358                                 break;
1359                         }
1360                         String albumTitle = configuration.getStringValue(albumPrefix + "/Title").getValue(null);
1361                         String albumDescription = configuration.getStringValue(albumPrefix + "/Description").getValue(null);
1362                         String albumParentId = configuration.getStringValue(albumPrefix + "/Parent").getValue(null);
1363                         String albumImageId = configuration.getStringValue(albumPrefix + "/AlbumImage").getValue(null);
1364                         if ((albumTitle == null) || (albumDescription == null)) {
1365                                 logger.log(Level.WARNING, "Invalid album found, aborting load!");
1366                                 return;
1367                         }
1368                         Album album = getAlbum(albumId).setSone(sone).setTitle(albumTitle).setDescription(albumDescription).setAlbumImage(albumImageId);
1369                         if (albumParentId != null) {
1370                                 Album parentAlbum = getAlbum(albumParentId, false);
1371                                 if (parentAlbum == null) {
1372                                         logger.log(Level.WARNING, String.format("Invalid parent album ID: %s", albumParentId));
1373                                         return;
1374                                 }
1375                                 parentAlbum.addAlbum(album);
1376                         } else {
1377                                 if (!topLevelAlbums.contains(album)) {
1378                                         topLevelAlbums.add(album);
1379                                 }
1380                         }
1381                 }
1382
1383                 /* load images. */
1384                 int imageCounter = 0;
1385                 while (true) {
1386                         String imagePrefix = sonePrefix + "/Images/" + imageCounter++;
1387                         String imageId = configuration.getStringValue(imagePrefix + "/ID").getValue(null);
1388                         if (imageId == null) {
1389                                 break;
1390                         }
1391                         String albumId = configuration.getStringValue(imagePrefix + "/Album").getValue(null);
1392                         String key = configuration.getStringValue(imagePrefix + "/Key").getValue(null);
1393                         String title = configuration.getStringValue(imagePrefix + "/Title").getValue(null);
1394                         String description = configuration.getStringValue(imagePrefix + "/Description").getValue(null);
1395                         Long creationTime = configuration.getLongValue(imagePrefix + "/CreationTime").getValue(null);
1396                         Integer width = configuration.getIntValue(imagePrefix + "/Width").getValue(null);
1397                         Integer height = configuration.getIntValue(imagePrefix + "/Height").getValue(null);
1398                         if ((albumId == null) || (key == null) || (title == null) || (description == null) || (creationTime == null) || (width == null) || (height == null)) {
1399                                 logger.log(Level.WARNING, "Invalid image found, aborting load!");
1400                                 return;
1401                         }
1402                         Album album = getAlbum(albumId, false);
1403                         if (album == null) {
1404                                 logger.log(Level.WARNING, "Invalid album image encountered, aborting load!");
1405                                 return;
1406                         }
1407                         Image image = getImage(imageId).setSone(sone).setCreationTime(creationTime).setKey(key);
1408                         image.setTitle(title).setDescription(description).setWidth(width).setHeight(height);
1409                         album.addImage(image);
1410                 }
1411
1412                 /* load avatar. */
1413                 String avatarId = configuration.getStringValue(sonePrefix + "/Profile/Avatar").getValue(null);
1414                 if (avatarId != null) {
1415                         profile.setAvatar(getImage(avatarId, false));
1416                 }
1417
1418                 /* load options. */
1419                 sone.getOptions().getBooleanOption("AutoFollow").set(configuration.getBooleanValue(sonePrefix + "/Options/AutoFollow").getValue(null));
1420                 sone.getOptions().getBooleanOption("EnableSoneInsertNotifications").set(configuration.getBooleanValue(sonePrefix + "/Options/EnableSoneInsertNotifications").getValue(null));
1421                 sone.getOptions().getBooleanOption("ShowNotification/NewSones").set(configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewSones").getValue(null));
1422                 sone.getOptions().getBooleanOption("ShowNotification/NewPosts").set(configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewPosts").getValue(null));
1423                 sone.getOptions().getBooleanOption("ShowNotification/NewReplies").set(configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewReplies").getValue(null));
1424                 sone.getOptions().<ShowCustomAvatars> getEnumOption("ShowCustomAvatars").set(ShowCustomAvatars.valueOf(configuration.getStringValue(sonePrefix + "/Options/ShowCustomAvatars").getValue(ShowCustomAvatars.NEVER.name())));
1425
1426                 /* if we’re still here, Sone was loaded successfully. */
1427                 synchronized (sone) {
1428                         sone.setTime(soneTime);
1429                         sone.setProfile(profile);
1430                         sone.setPosts(posts);
1431                         sone.setReplies(replies);
1432                         sone.setLikePostIds(likedPostIds);
1433                         sone.setLikeReplyIds(likedReplyIds);
1434                         for (String friendId : friends) {
1435                                 followSone(sone, friendId);
1436                         }
1437                         sone.setAlbums(topLevelAlbums);
1438                         soneInserters.get(sone).setLastInsertFingerprint(lastInsertFingerprint);
1439                 }
1440                 synchronized (knownSones) {
1441                         for (String friend : friends) {
1442                                 knownSones.add(friend);
1443                         }
1444                 }
1445                 synchronized (knownPosts) {
1446                         for (Post post : posts) {
1447                                 knownPosts.add(post.getId());
1448                         }
1449                 }
1450                 synchronized (knownReplies) {
1451                         for (PostReply reply : replies) {
1452                                 knownReplies.add(reply.getId());
1453                         }
1454                 }
1455         }
1456
1457         /**
1458          * Creates a new post.
1459          *
1460          * @param sone
1461          *            The Sone that creates the post
1462          * @param text
1463          *            The text of the post
1464          * @return The created post
1465          */
1466         public Post createPost(Sone sone, String text) {
1467                 return createPost(sone, System.currentTimeMillis(), text);
1468         }
1469
1470         /**
1471          * Creates a new post.
1472          *
1473          * @param sone
1474          *            The Sone that creates the post
1475          * @param time
1476          *            The time of the post
1477          * @param text
1478          *            The text of the post
1479          * @return The created post
1480          */
1481         public Post createPost(Sone sone, long time, String text) {
1482                 return createPost(sone, null, time, text);
1483         }
1484
1485         /**
1486          * Creates a new post.
1487          *
1488          * @param sone
1489          *            The Sone that creates the post
1490          * @param recipient
1491          *            The recipient Sone, or {@code null} if this post does not have
1492          *            a recipient
1493          * @param text
1494          *            The text of the post
1495          * @return The created post
1496          */
1497         public Post createPost(Sone sone, Sone recipient, String text) {
1498                 return createPost(sone, recipient, System.currentTimeMillis(), text);
1499         }
1500
1501         /**
1502          * Creates a new post.
1503          *
1504          * @param sone
1505          *            The Sone that creates the post
1506          * @param recipient
1507          *            The recipient Sone, or {@code null} if this post does not have
1508          *            a recipient
1509          * @param time
1510          *            The time of the post
1511          * @param text
1512          *            The text of the post
1513          * @return The created post
1514          */
1515         public Post createPost(Sone sone, Sone recipient, long time, String text) {
1516                 Validation.begin().isNotNull("Text", text).check().isGreater("Text Length", text.length(), 0).check();
1517                 if (!sone.isLocal()) {
1518                         logger.log(Level.FINE, String.format("Tried to create post for non-local Sone: %s", sone));
1519                         return null;
1520                 }
1521                 final Post post = new PostImpl(sone, time, text);
1522                 if (recipient != null) {
1523                         post.setRecipient(recipient);
1524                 }
1525                 synchronized (posts) {
1526                         posts.put(post.getId(), post);
1527                 }
1528                 eventBus.post(new NewPostFoundEvent(post));
1529                 sone.addPost(post);
1530                 touchConfiguration();
1531                 localElementTicker.registerEvent(System.currentTimeMillis() + 10 * 1000, new Runnable() {
1532
1533                         /**
1534                          * {@inheritDoc}
1535                          */
1536                         @Override
1537                         public void run() {
1538                                 markPostKnown(post);
1539                         }
1540                 }, "Mark " + post + " read.");
1541                 return post;
1542         }
1543
1544         /**
1545          * Deletes the given post.
1546          *
1547          * @param post
1548          *            The post to delete
1549          */
1550         public void deletePost(Post post) {
1551                 if (!post.getSone().isLocal()) {
1552                         logger.log(Level.WARNING, String.format("Tried to delete post of non-local Sone: %s", post.getSone()));
1553                         return;
1554                 }
1555                 post.getSone().removePost(post);
1556                 synchronized (posts) {
1557                         posts.remove(post.getId());
1558                 }
1559                 eventBus.post(new PostRemovedEvent(post));
1560                 markPostKnown(post);
1561                 touchConfiguration();
1562         }
1563
1564         /**
1565          * Marks the given post as known, if it is currently not a known post
1566          * (according to {@link Post#isKnown()}).
1567          *
1568          * @param post
1569          *            The post to mark as known
1570          */
1571         public void markPostKnown(Post post) {
1572                 post.setKnown(true);
1573                 synchronized (knownPosts) {
1574                         eventBus.post(new MarkPostKnownEvent(post));
1575                         if (knownPosts.add(post.getId())) {
1576                                 touchConfiguration();
1577                         }
1578                 }
1579                 for (PostReply reply : getReplies(post)) {
1580                         markReplyKnown(reply);
1581                 }
1582         }
1583
1584         /**
1585          * Bookmarks the given post.
1586          *
1587          * @param post
1588          *            The post to bookmark
1589          */
1590         public void bookmark(Post post) {
1591                 bookmarkPost(post.getId());
1592         }
1593
1594         /**
1595          * Bookmarks the post with the given ID.
1596          *
1597          * @param id
1598          *            The ID of the post to bookmark
1599          */
1600         public void bookmarkPost(String id) {
1601                 synchronized (bookmarkedPosts) {
1602                         bookmarkedPosts.add(id);
1603                 }
1604         }
1605
1606         /**
1607          * Removes the given post from the bookmarks.
1608          *
1609          * @param post
1610          *            The post to unbookmark
1611          */
1612         public void unbookmark(Post post) {
1613                 unbookmarkPost(post.getId());
1614         }
1615
1616         /**
1617          * Removes the post with the given ID from the bookmarks.
1618          *
1619          * @param id
1620          *            The ID of the post to unbookmark
1621          */
1622         public void unbookmarkPost(String id) {
1623                 synchronized (bookmarkedPosts) {
1624                         bookmarkedPosts.remove(id);
1625                 }
1626         }
1627
1628         /**
1629          * Creates a new reply.
1630          *
1631          * @param sone
1632          *            The Sone that creates the reply
1633          * @param post
1634          *            The post that this reply refers to
1635          * @param text
1636          *            The text of the reply
1637          * @return The created reply
1638          */
1639         public PostReply createReply(Sone sone, Post post, String text) {
1640                 return createReply(sone, post, System.currentTimeMillis(), text);
1641         }
1642
1643         /**
1644          * Creates a new reply.
1645          *
1646          * @param sone
1647          *            The Sone that creates the reply
1648          * @param post
1649          *            The post that this reply refers to
1650          * @param time
1651          *            The time of the reply
1652          * @param text
1653          *            The text of the reply
1654          * @return The created reply
1655          */
1656         public PostReply createReply(Sone sone, Post post, long time, String text) {
1657                 Validation.begin().isNotNull("Text", text).check().isGreater("Text Length", text.trim().length(), 0).check();
1658                 if (!sone.isLocal()) {
1659                         logger.log(Level.FINE, String.format("Tried to create reply for non-local Sone: %s", sone));
1660                         return null;
1661                 }
1662                 final PostReply reply = new PostReply(sone, post, System.currentTimeMillis(), text);
1663                 synchronized (replies) {
1664                         replies.put(reply.getId(), reply);
1665                 }
1666                 synchronized (knownReplies) {
1667                         eventBus.post(new NewPostReplyFoundEvent(reply));
1668                 }
1669                 sone.addReply(reply);
1670                 touchConfiguration();
1671                 localElementTicker.registerEvent(System.currentTimeMillis() + 10 * 1000, new Runnable() {
1672
1673                         /**
1674                          * {@inheritDoc}
1675                          */
1676                         @Override
1677                         public void run() {
1678                                 markReplyKnown(reply);
1679                         }
1680                 }, "Mark " + reply + " read.");
1681                 return reply;
1682         }
1683
1684         /**
1685          * Deletes the given reply.
1686          *
1687          * @param reply
1688          *            The reply to delete
1689          */
1690         public void deleteReply(PostReply reply) {
1691                 Sone sone = reply.getSone();
1692                 if (!sone.isLocal()) {
1693                         logger.log(Level.FINE, String.format("Tried to delete non-local reply: %s", reply));
1694                         return;
1695                 }
1696                 synchronized (replies) {
1697                         replies.remove(reply.getId());
1698                 }
1699                 synchronized (knownReplies) {
1700                         markReplyKnown(reply);
1701                         knownReplies.remove(reply.getId());
1702                 }
1703                 sone.removeReply(reply);
1704                 touchConfiguration();
1705         }
1706
1707         /**
1708          * Marks the given reply as known, if it is currently not a known reply
1709          * (according to {@link Reply#isKnown()}).
1710          *
1711          * @param reply
1712          *            The reply to mark as known
1713          */
1714         public void markReplyKnown(PostReply reply) {
1715                 reply.setKnown(true);
1716                 synchronized (knownReplies) {
1717                         eventBus.post(new MarkPostReplyKnownEvent(reply));
1718                         if (knownReplies.add(reply.getId())) {
1719                                 touchConfiguration();
1720                         }
1721                 }
1722         }
1723
1724         /**
1725          * Creates a new top-level album for the given Sone.
1726          *
1727          * @param sone
1728          *            The Sone to create the album for
1729          * @return The new album
1730          */
1731         public Album createAlbum(Sone sone) {
1732                 return createAlbum(sone, null);
1733         }
1734
1735         /**
1736          * Creates a new album for the given Sone.
1737          *
1738          * @param sone
1739          *            The Sone to create the album for
1740          * @param parent
1741          *            The parent of the album (may be {@code null} to create a
1742          *            top-level album)
1743          * @return The new album
1744          */
1745         public Album createAlbum(Sone sone, Album parent) {
1746                 Album album = new Album();
1747                 synchronized (albums) {
1748                         albums.put(album.getId(), album);
1749                 }
1750                 album.setSone(sone);
1751                 if (parent != null) {
1752                         parent.addAlbum(album);
1753                 } else {
1754                         sone.addAlbum(album);
1755                 }
1756                 return album;
1757         }
1758
1759         /**
1760          * Deletes the given album. The owner of the album has to be a local Sone,
1761          * and the album has to be {@link Album#isEmpty() empty} to be deleted.
1762          *
1763          * @param album
1764          *            The album to remove
1765          */
1766         public void deleteAlbum(Album album) {
1767                 Validation.begin().isNotNull("Album", album).check().is("Local Sone", album.getSone().isLocal()).check();
1768                 if (!album.isEmpty()) {
1769                         return;
1770                 }
1771                 if (album.getParent() == null) {
1772                         album.getSone().removeAlbum(album);
1773                 } else {
1774                         album.getParent().removeAlbum(album);
1775                 }
1776                 synchronized (albums) {
1777                         albums.remove(album.getId());
1778                 }
1779                 touchConfiguration();
1780         }
1781
1782         /**
1783          * Creates a new image.
1784          *
1785          * @param sone
1786          *            The Sone creating the image
1787          * @param album
1788          *            The album the image will be inserted into
1789          * @param temporaryImage
1790          *            The temporary image to create the image from
1791          * @return The newly created image
1792          */
1793         public Image createImage(Sone sone, Album album, TemporaryImage temporaryImage) {
1794                 Validation.begin().isNotNull("Sone", sone).isNotNull("Album", album).isNotNull("Temporary Image", temporaryImage).check().is("Local Sone", sone.isLocal()).check().isEqual("Owner and Album Owner", sone, album.getSone()).check();
1795                 Image image = new Image(temporaryImage.getId()).setSone(sone).setCreationTime(System.currentTimeMillis());
1796                 album.addImage(image);
1797                 synchronized (images) {
1798                         images.put(image.getId(), image);
1799                 }
1800                 imageInserter.insertImage(temporaryImage, image);
1801                 return image;
1802         }
1803
1804         /**
1805          * Deletes the given image. This method will also delete a matching
1806          * temporary image.
1807          *
1808          * @see #deleteTemporaryImage(TemporaryImage)
1809          * @param image
1810          *            The image to delete
1811          */
1812         public void deleteImage(Image image) {
1813                 Validation.begin().isNotNull("Image", image).check().is("Local Sone", image.getSone().isLocal()).check();
1814                 deleteTemporaryImage(image.getId());
1815                 image.getAlbum().removeImage(image);
1816                 synchronized (images) {
1817                         images.remove(image.getId());
1818                 }
1819                 touchConfiguration();
1820         }
1821
1822         /**
1823          * Creates a new temporary image.
1824          *
1825          * @param mimeType
1826          *            The MIME type of the temporary image
1827          * @param imageData
1828          *            The encoded data of the image
1829          * @return The temporary image
1830          */
1831         public TemporaryImage createTemporaryImage(String mimeType, byte[] imageData) {
1832                 TemporaryImage temporaryImage = new TemporaryImage();
1833                 temporaryImage.setMimeType(mimeType).setImageData(imageData);
1834                 synchronized (temporaryImages) {
1835                         temporaryImages.put(temporaryImage.getId(), temporaryImage);
1836                 }
1837                 return temporaryImage;
1838         }
1839
1840         /**
1841          * Deletes the given temporary image.
1842          *
1843          * @param temporaryImage
1844          *            The temporary image to delete
1845          */
1846         public void deleteTemporaryImage(TemporaryImage temporaryImage) {
1847                 Validation.begin().isNotNull("Temporary Image", temporaryImage).check();
1848                 deleteTemporaryImage(temporaryImage.getId());
1849         }
1850
1851         /**
1852          * Deletes the temporary image with the given ID.
1853          *
1854          * @param imageId
1855          *            The ID of the temporary image to delete
1856          */
1857         public void deleteTemporaryImage(String imageId) {
1858                 Validation.begin().isNotNull("Temporary Image ID", imageId).check();
1859                 synchronized (temporaryImages) {
1860                         temporaryImages.remove(imageId);
1861                 }
1862                 Image image = getImage(imageId, false);
1863                 if (image != null) {
1864                         imageInserter.cancelImageInsert(image);
1865                 }
1866         }
1867
1868         /**
1869          * Notifies the core that the configuration, either of the core or of a
1870          * single local Sone, has changed, and that the configuration should be
1871          * saved.
1872          */
1873         public void touchConfiguration() {
1874                 lastConfigurationUpdate = System.currentTimeMillis();
1875         }
1876
1877         //
1878         // SERVICE METHODS
1879         //
1880
1881         /**
1882          * Starts the core.
1883          */
1884         @Override
1885         public void serviceStart() {
1886                 loadConfiguration();
1887                 updateChecker.start();
1888                 identityManager.addIdentityListener(this);
1889                 identityManager.start();
1890                 webOfTrustUpdater.init();
1891                 webOfTrustUpdater.start();
1892         }
1893
1894         /**
1895          * {@inheritDoc}
1896          */
1897         @Override
1898         public void serviceRun() {
1899                 long lastSaved = System.currentTimeMillis();
1900                 while (!shouldStop()) {
1901                         sleep(1000);
1902                         long now = System.currentTimeMillis();
1903                         if (shouldStop() || ((lastConfigurationUpdate > lastSaved) && ((now - lastConfigurationUpdate) > 5000))) {
1904                                 for (Sone localSone : getLocalSones()) {
1905                                         saveSone(localSone);
1906                                 }
1907                                 saveConfiguration();
1908                                 lastSaved = now;
1909                         }
1910                 }
1911         }
1912
1913         /**
1914          * Stops the core.
1915          */
1916         @Override
1917         public void serviceStop() {
1918                 synchronized (sones) {
1919                         for (Entry<Sone, SoneInserter> soneInserter : soneInserters.entrySet()) {
1920                                 soneInserter.getValue().stop();
1921                                 saveSone(soneInserter.getKey());
1922                         }
1923                 }
1924                 saveConfiguration();
1925                 webOfTrustUpdater.stop();
1926                 updateChecker.stop();
1927                 soneDownloader.stop();
1928                 identityManager.removeIdentityListener(this);
1929                 identityManager.stop();
1930         }
1931
1932         //
1933         // PRIVATE METHODS
1934         //
1935
1936         /**
1937          * Saves the given Sone. This will persist all local settings for the given
1938          * Sone, such as the friends list and similar, private options.
1939          *
1940          * @param sone
1941          *            The Sone to save
1942          */
1943         private synchronized void saveSone(Sone sone) {
1944                 if (!sone.isLocal()) {
1945                         logger.log(Level.FINE, String.format("Tried to save non-local Sone: %s", sone));
1946                         return;
1947                 }
1948                 if (!(sone.getIdentity() instanceof OwnIdentity)) {
1949                         logger.log(Level.WARNING, String.format("Local Sone without OwnIdentity found, refusing to save: %s", sone));
1950                         return;
1951                 }
1952
1953                 logger.log(Level.INFO, String.format("Saving Sone: %s", sone));
1954                 try {
1955                         /* save Sone into configuration. */
1956                         String sonePrefix = "Sone/" + sone.getId();
1957                         configuration.getLongValue(sonePrefix + "/Time").setValue(sone.getTime());
1958                         configuration.getStringValue(sonePrefix + "/LastInsertFingerprint").setValue(soneInserters.get(sone).getLastInsertFingerprint());
1959
1960                         /* save profile. */
1961                         Profile profile = sone.getProfile();
1962                         configuration.getStringValue(sonePrefix + "/Profile/FirstName").setValue(profile.getFirstName());
1963                         configuration.getStringValue(sonePrefix + "/Profile/MiddleName").setValue(profile.getMiddleName());
1964                         configuration.getStringValue(sonePrefix + "/Profile/LastName").setValue(profile.getLastName());
1965                         configuration.getIntValue(sonePrefix + "/Profile/BirthDay").setValue(profile.getBirthDay());
1966                         configuration.getIntValue(sonePrefix + "/Profile/BirthMonth").setValue(profile.getBirthMonth());
1967                         configuration.getIntValue(sonePrefix + "/Profile/BirthYear").setValue(profile.getBirthYear());
1968                         configuration.getStringValue(sonePrefix + "/Profile/Avatar").setValue(profile.getAvatar());
1969
1970                         /* save profile fields. */
1971                         int fieldCounter = 0;
1972                         for (Field profileField : profile.getFields()) {
1973                                 String fieldPrefix = sonePrefix + "/Profile/Fields/" + fieldCounter++;
1974                                 configuration.getStringValue(fieldPrefix + "/Name").setValue(profileField.getName());
1975                                 configuration.getStringValue(fieldPrefix + "/Value").setValue(profileField.getValue());
1976                         }
1977                         configuration.getStringValue(sonePrefix + "/Profile/Fields/" + fieldCounter + "/Name").setValue(null);
1978
1979                         /* save posts. */
1980                         int postCounter = 0;
1981                         for (Post post : sone.getPosts()) {
1982                                 String postPrefix = sonePrefix + "/Posts/" + postCounter++;
1983                                 configuration.getStringValue(postPrefix + "/ID").setValue(post.getId());
1984                                 configuration.getStringValue(postPrefix + "/Recipient").setValue((post.getRecipient() != null) ? post.getRecipient().getId() : null);
1985                                 configuration.getLongValue(postPrefix + "/Time").setValue(post.getTime());
1986                                 configuration.getStringValue(postPrefix + "/Text").setValue(post.getText());
1987                         }
1988                         configuration.getStringValue(sonePrefix + "/Posts/" + postCounter + "/ID").setValue(null);
1989
1990                         /* save replies. */
1991                         int replyCounter = 0;
1992                         for (PostReply reply : sone.getReplies()) {
1993                                 String replyPrefix = sonePrefix + "/Replies/" + replyCounter++;
1994                                 configuration.getStringValue(replyPrefix + "/ID").setValue(reply.getId());
1995                                 configuration.getStringValue(replyPrefix + "/Post/ID").setValue(reply.getPost().getId());
1996                                 configuration.getLongValue(replyPrefix + "/Time").setValue(reply.getTime());
1997                                 configuration.getStringValue(replyPrefix + "/Text").setValue(reply.getText());
1998                         }
1999                         configuration.getStringValue(sonePrefix + "/Replies/" + replyCounter + "/ID").setValue(null);
2000
2001                         /* save post likes. */
2002                         int postLikeCounter = 0;
2003                         for (String postId : sone.getLikedPostIds()) {
2004                                 configuration.getStringValue(sonePrefix + "/Likes/Post/" + postLikeCounter++ + "/ID").setValue(postId);
2005                         }
2006                         configuration.getStringValue(sonePrefix + "/Likes/Post/" + postLikeCounter + "/ID").setValue(null);
2007
2008                         /* save reply likes. */
2009                         int replyLikeCounter = 0;
2010                         for (String replyId : sone.getLikedReplyIds()) {
2011                                 configuration.getStringValue(sonePrefix + "/Likes/Reply/" + replyLikeCounter++ + "/ID").setValue(replyId);
2012                         }
2013                         configuration.getStringValue(sonePrefix + "/Likes/Reply/" + replyLikeCounter + "/ID").setValue(null);
2014
2015                         /* save friends. */
2016                         int friendCounter = 0;
2017                         for (String friendId : sone.getFriends()) {
2018                                 configuration.getStringValue(sonePrefix + "/Friends/" + friendCounter++ + "/ID").setValue(friendId);
2019                         }
2020                         configuration.getStringValue(sonePrefix + "/Friends/" + friendCounter + "/ID").setValue(null);
2021
2022                         /* save albums. first, collect in a flat structure, top-level first. */
2023                         List<Album> albums = sone.getAllAlbums();
2024
2025                         int albumCounter = 0;
2026                         for (Album album : albums) {
2027                                 String albumPrefix = sonePrefix + "/Albums/" + albumCounter++;
2028                                 configuration.getStringValue(albumPrefix + "/ID").setValue(album.getId());
2029                                 configuration.getStringValue(albumPrefix + "/Title").setValue(album.getTitle());
2030                                 configuration.getStringValue(albumPrefix + "/Description").setValue(album.getDescription());
2031                                 configuration.getStringValue(albumPrefix + "/Parent").setValue(album.getParent() == null ? null : album.getParent().getId());
2032                                 configuration.getStringValue(albumPrefix + "/AlbumImage").setValue(album.getAlbumImage() == null ? null : album.getAlbumImage().getId());
2033                         }
2034                         configuration.getStringValue(sonePrefix + "/Albums/" + albumCounter + "/ID").setValue(null);
2035
2036                         /* save images. */
2037                         int imageCounter = 0;
2038                         for (Album album : albums) {
2039                                 for (Image image : album.getImages()) {
2040                                         if (!image.isInserted()) {
2041                                                 continue;
2042                                         }
2043                                         String imagePrefix = sonePrefix + "/Images/" + imageCounter++;
2044                                         configuration.getStringValue(imagePrefix + "/ID").setValue(image.getId());
2045                                         configuration.getStringValue(imagePrefix + "/Album").setValue(album.getId());
2046                                         configuration.getStringValue(imagePrefix + "/Key").setValue(image.getKey());
2047                                         configuration.getStringValue(imagePrefix + "/Title").setValue(image.getTitle());
2048                                         configuration.getStringValue(imagePrefix + "/Description").setValue(image.getDescription());
2049                                         configuration.getLongValue(imagePrefix + "/CreationTime").setValue(image.getCreationTime());
2050                                         configuration.getIntValue(imagePrefix + "/Width").setValue(image.getWidth());
2051                                         configuration.getIntValue(imagePrefix + "/Height").setValue(image.getHeight());
2052                                 }
2053                         }
2054                         configuration.getStringValue(sonePrefix + "/Images/" + imageCounter + "/ID").setValue(null);
2055
2056                         /* save options. */
2057                         configuration.getBooleanValue(sonePrefix + "/Options/AutoFollow").setValue(sone.getOptions().getBooleanOption("AutoFollow").getReal());
2058                         configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewSones").setValue(sone.getOptions().getBooleanOption("ShowNotification/NewSones").getReal());
2059                         configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewPosts").setValue(sone.getOptions().getBooleanOption("ShowNotification/NewPosts").getReal());
2060                         configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewReplies").setValue(sone.getOptions().getBooleanOption("ShowNotification/NewReplies").getReal());
2061                         configuration.getBooleanValue(sonePrefix + "/Options/EnableSoneInsertNotifications").setValue(sone.getOptions().getBooleanOption("EnableSoneInsertNotifications").getReal());
2062                         configuration.getStringValue(sonePrefix + "/Options/ShowCustomAvatars").setValue(sone.getOptions().<ShowCustomAvatars> getEnumOption("ShowCustomAvatars").get().name());
2063
2064                         configuration.save();
2065
2066                         webOfTrustUpdater.setProperty((OwnIdentity) sone.getIdentity(), "Sone.LatestEdition", String.valueOf(sone.getLatestEdition()));
2067
2068                         logger.log(Level.INFO, String.format("Sone %s saved.", sone));
2069                 } catch (ConfigurationException ce1) {
2070                         logger.log(Level.WARNING, String.format("Could not save Sone: %s", sone), ce1);
2071                 }
2072         }
2073
2074         /**
2075          * Saves the current options.
2076          */
2077         private void saveConfiguration() {
2078                 synchronized (configuration) {
2079                         if (storingConfiguration) {
2080                                 logger.log(Level.FINE, "Already storing configuration…");
2081                                 return;
2082                         }
2083                         storingConfiguration = true;
2084                 }
2085
2086                 /* store the options first. */
2087                 try {
2088                         configuration.getIntValue("Option/ConfigurationVersion").setValue(0);
2089                         configuration.getIntValue("Option/InsertionDelay").setValue(options.getIntegerOption("InsertionDelay").getReal());
2090                         configuration.getIntValue("Option/PostsPerPage").setValue(options.getIntegerOption("PostsPerPage").getReal());
2091                         configuration.getIntValue("Option/ImagesPerPage").setValue(options.getIntegerOption("ImagesPerPage").getReal());
2092                         configuration.getIntValue("Option/CharactersPerPost").setValue(options.getIntegerOption("CharactersPerPost").getReal());
2093                         configuration.getIntValue("Option/PostCutOffLength").setValue(options.getIntegerOption("PostCutOffLength").getReal());
2094                         configuration.getBooleanValue("Option/RequireFullAccess").setValue(options.getBooleanOption("RequireFullAccess").getReal());
2095                         configuration.getIntValue("Option/PositiveTrust").setValue(options.getIntegerOption("PositiveTrust").getReal());
2096                         configuration.getIntValue("Option/NegativeTrust").setValue(options.getIntegerOption("NegativeTrust").getReal());
2097                         configuration.getStringValue("Option/TrustComment").setValue(options.getStringOption("TrustComment").getReal());
2098                         configuration.getBooleanValue("Option/ActivateFcpInterface").setValue(options.getBooleanOption("ActivateFcpInterface").getReal());
2099                         configuration.getIntValue("Option/FcpFullAccessRequired").setValue(options.getIntegerOption("FcpFullAccessRequired").getReal());
2100
2101                         /* save known Sones. */
2102                         int soneCounter = 0;
2103                         synchronized (knownSones) {
2104                                 for (String knownSoneId : knownSones) {
2105                                         configuration.getStringValue("KnownSone/" + soneCounter++ + "/ID").setValue(knownSoneId);
2106                                 }
2107                                 configuration.getStringValue("KnownSone/" + soneCounter + "/ID").setValue(null);
2108                         }
2109
2110                         /* save Sone following times. */
2111                         soneCounter = 0;
2112                         synchronized (soneFollowingTimes) {
2113                                 for (Entry<Sone, Long> soneFollowingTime : soneFollowingTimes.entrySet()) {
2114                                         configuration.getStringValue("SoneFollowingTimes/" + soneCounter + "/Sone").setValue(soneFollowingTime.getKey().getId());
2115                                         configuration.getLongValue("SoneFollowingTimes/" + soneCounter + "/Time").setValue(soneFollowingTime.getValue());
2116                                         ++soneCounter;
2117                                 }
2118                                 configuration.getStringValue("SoneFollowingTimes/" + soneCounter + "/Sone").setValue(null);
2119                         }
2120
2121                         /* save known posts. */
2122                         int postCounter = 0;
2123                         synchronized (knownPosts) {
2124                                 for (String knownPostId : knownPosts) {
2125                                         configuration.getStringValue("KnownPosts/" + postCounter++ + "/ID").setValue(knownPostId);
2126                                 }
2127                                 configuration.getStringValue("KnownPosts/" + postCounter + "/ID").setValue(null);
2128                         }
2129
2130                         /* save known replies. */
2131                         int replyCounter = 0;
2132                         synchronized (knownReplies) {
2133                                 for (String knownReplyId : knownReplies) {
2134                                         configuration.getStringValue("KnownReplies/" + replyCounter++ + "/ID").setValue(knownReplyId);
2135                                 }
2136                                 configuration.getStringValue("KnownReplies/" + replyCounter + "/ID").setValue(null);
2137                         }
2138
2139                         /* save bookmarked posts. */
2140                         int bookmarkedPostCounter = 0;
2141                         synchronized (bookmarkedPosts) {
2142                                 for (String bookmarkedPostId : bookmarkedPosts) {
2143                                         configuration.getStringValue("Bookmarks/Post/" + bookmarkedPostCounter++ + "/ID").setValue(bookmarkedPostId);
2144                                 }
2145                         }
2146                         configuration.getStringValue("Bookmarks/Post/" + bookmarkedPostCounter++ + "/ID").setValue(null);
2147
2148                         /* now save it. */
2149                         configuration.save();
2150
2151                 } catch (ConfigurationException ce1) {
2152                         logger.log(Level.SEVERE, "Could not store configuration!", ce1);
2153                 } finally {
2154                         synchronized (configuration) {
2155                                 storingConfiguration = false;
2156                         }
2157                 }
2158         }
2159
2160         /**
2161          * Loads the configuration.
2162          */
2163         @SuppressWarnings("unchecked")
2164         private void loadConfiguration() {
2165                 /* create options. */
2166                 options.addIntegerOption("InsertionDelay", new DefaultOption<Integer>(60, new IntegerRangeValidator(0, Integer.MAX_VALUE), new OptionWatcher<Integer>() {
2167
2168                         @Override
2169                         public void optionChanged(Option<Integer> option, Integer oldValue, Integer newValue) {
2170                                 SoneInserter.setInsertionDelay(newValue);
2171                         }
2172
2173                 }));
2174                 options.addIntegerOption("PostsPerPage", new DefaultOption<Integer>(10, new IntegerRangeValidator(1, Integer.MAX_VALUE)));
2175                 options.addIntegerOption("ImagesPerPage", new DefaultOption<Integer>(9, new IntegerRangeValidator(1, Integer.MAX_VALUE)));
2176                 options.addIntegerOption("CharactersPerPost", new DefaultOption<Integer>(400, new OrValidator<Integer>(new IntegerRangeValidator(50, Integer.MAX_VALUE), new EqualityValidator<Integer>(-1))));
2177                 options.addIntegerOption("PostCutOffLength", new DefaultOption<Integer>(200, new OrValidator<Integer>(new IntegerRangeValidator(50, Integer.MAX_VALUE), new EqualityValidator<Integer>(-1))));
2178                 options.addBooleanOption("RequireFullAccess", new DefaultOption<Boolean>(false));
2179                 options.addIntegerOption("PositiveTrust", new DefaultOption<Integer>(75, new IntegerRangeValidator(0, 100)));
2180                 options.addIntegerOption("NegativeTrust", new DefaultOption<Integer>(-25, new IntegerRangeValidator(-100, 100)));
2181                 options.addStringOption("TrustComment", new DefaultOption<String>("Set from Sone Web Interface"));
2182                 options.addBooleanOption("ActivateFcpInterface", new DefaultOption<Boolean>(false, new OptionWatcher<Boolean>() {
2183
2184                         @Override
2185                         @SuppressWarnings("synthetic-access")
2186                         public void optionChanged(Option<Boolean> option, Boolean oldValue, Boolean newValue) {
2187                                 fcpInterface.setActive(newValue);
2188                         }
2189                 }));
2190                 options.addIntegerOption("FcpFullAccessRequired", new DefaultOption<Integer>(2, new OptionWatcher<Integer>() {
2191
2192                         @Override
2193                         @SuppressWarnings("synthetic-access")
2194                         public void optionChanged(Option<Integer> option, Integer oldValue, Integer newValue) {
2195                                 fcpInterface.setFullAccessRequired(FullAccessRequired.values()[newValue]);
2196                         }
2197
2198                 }));
2199
2200                 loadConfigurationValue("InsertionDelay");
2201                 loadConfigurationValue("PostsPerPage");
2202                 loadConfigurationValue("ImagesPerPage");
2203                 loadConfigurationValue("CharactersPerPost");
2204                 loadConfigurationValue("PostCutOffLength");
2205                 options.getBooleanOption("RequireFullAccess").set(configuration.getBooleanValue("Option/RequireFullAccess").getValue(null));
2206                 loadConfigurationValue("PositiveTrust");
2207                 loadConfigurationValue("NegativeTrust");
2208                 options.getStringOption("TrustComment").set(configuration.getStringValue("Option/TrustComment").getValue(null));
2209                 options.getBooleanOption("ActivateFcpInterface").set(configuration.getBooleanValue("Option/ActivateFcpInterface").getValue(null));
2210                 options.getIntegerOption("FcpFullAccessRequired").set(configuration.getIntValue("Option/FcpFullAccessRequired").getValue(null));
2211
2212                 /* load known Sones. */
2213                 int soneCounter = 0;
2214                 while (true) {
2215                         String knownSoneId = configuration.getStringValue("KnownSone/" + soneCounter++ + "/ID").getValue(null);
2216                         if (knownSoneId == null) {
2217                                 break;
2218                         }
2219                         synchronized (knownSones) {
2220                                 knownSones.add(knownSoneId);
2221                         }
2222                 }
2223
2224                 /* load Sone following times. */
2225                 soneCounter = 0;
2226                 while (true) {
2227                         String soneId = configuration.getStringValue("SoneFollowingTimes/" + soneCounter + "/Sone").getValue(null);
2228                         if (soneId == null) {
2229                                 break;
2230                         }
2231                         long time = configuration.getLongValue("SoneFollowingTimes/" + soneCounter + "/Time").getValue(Long.MAX_VALUE);
2232                         Sone followedSone = getSone(soneId);
2233                         if (followedSone == null) {
2234                                 logger.log(Level.WARNING, String.format("Ignoring Sone with invalid ID: %s", soneId));
2235                         } else {
2236                                 synchronized (soneFollowingTimes) {
2237                                         soneFollowingTimes.put(getSone(soneId), time);
2238                                 }
2239                         }
2240                         ++soneCounter;
2241                 }
2242
2243                 /* load known posts. */
2244                 int postCounter = 0;
2245                 while (true) {
2246                         String knownPostId = configuration.getStringValue("KnownPosts/" + postCounter++ + "/ID").getValue(null);
2247                         if (knownPostId == null) {
2248                                 break;
2249                         }
2250                         synchronized (knownPosts) {
2251                                 knownPosts.add(knownPostId);
2252                         }
2253                 }
2254
2255                 /* load known replies. */
2256                 int replyCounter = 0;
2257                 while (true) {
2258                         String knownReplyId = configuration.getStringValue("KnownReplies/" + replyCounter++ + "/ID").getValue(null);
2259                         if (knownReplyId == null) {
2260                                 break;
2261                         }
2262                         synchronized (knownReplies) {
2263                                 knownReplies.add(knownReplyId);
2264                         }
2265                 }
2266
2267                 /* load bookmarked posts. */
2268                 int bookmarkedPostCounter = 0;
2269                 while (true) {
2270                         String bookmarkedPostId = configuration.getStringValue("Bookmarks/Post/" + bookmarkedPostCounter++ + "/ID").getValue(null);
2271                         if (bookmarkedPostId == null) {
2272                                 break;
2273                         }
2274                         synchronized (bookmarkedPosts) {
2275                                 bookmarkedPosts.add(bookmarkedPostId);
2276                         }
2277                 }
2278
2279         }
2280
2281         /**
2282          * Loads an {@link Integer} configuration value for the option with the
2283          * given name, logging validation failures.
2284          *
2285          * @param optionName
2286          *            The name of the option to load
2287          */
2288         private void loadConfigurationValue(String optionName) {
2289                 try {
2290                         options.getIntegerOption(optionName).set(configuration.getIntValue("Option/" + optionName).getValue(null));
2291                 } catch (IllegalArgumentException iae1) {
2292                         logger.log(Level.WARNING, String.format("Invalid value for %s in configuration, using default.", optionName));
2293                 }
2294         }
2295
2296         /**
2297          * Generate a Sone URI from the given URI and latest edition.
2298          *
2299          * @param uriString
2300          *            The URI to derive the Sone URI from
2301          * @return The derived URI
2302          */
2303         private static FreenetURI getSoneUri(String uriString) {
2304                 try {
2305                         FreenetURI uri = new FreenetURI(uriString).setDocName("Sone").setMetaString(new String[0]);
2306                         return uri;
2307                 } catch (MalformedURLException mue1) {
2308                         logger.log(Level.WARNING, String.format("Could not create Sone URI from URI: %s", uriString), mue1);
2309                         return null;
2310                 }
2311         }
2312
2313         //
2314         // INTERFACE IdentityListener
2315         //
2316
2317         /**
2318          * {@inheritDoc}
2319          */
2320         @Override
2321         public void ownIdentityAdded(OwnIdentity ownIdentity) {
2322                 logger.log(Level.FINEST, String.format("Adding OwnIdentity: %s", ownIdentity));
2323                 if (ownIdentity.hasContext("Sone")) {
2324                         trustedIdentities.put(ownIdentity, Collections.synchronizedSet(new HashSet<Identity>()));
2325                         addLocalSone(ownIdentity);
2326                 }
2327         }
2328
2329         /**
2330          * {@inheritDoc}
2331          */
2332         @Override
2333         public void ownIdentityRemoved(OwnIdentity ownIdentity) {
2334                 logger.log(Level.FINEST, String.format("Removing OwnIdentity: %s", ownIdentity));
2335                 trustedIdentities.remove(ownIdentity);
2336         }
2337
2338         /**
2339          * {@inheritDoc}
2340          */
2341         @Override
2342         public void identityAdded(OwnIdentity ownIdentity, Identity identity) {
2343                 logger.log(Level.FINEST, String.format("Adding Identity: %s", identity));
2344                 trustedIdentities.get(ownIdentity).add(identity);
2345                 addRemoteSone(identity);
2346         }
2347
2348         /**
2349          * {@inheritDoc}
2350          */
2351         @Override
2352         public void identityUpdated(OwnIdentity ownIdentity, final Identity identity) {
2353                 soneDownloaders.execute(new Runnable() {
2354
2355                         @Override
2356                         @SuppressWarnings("synthetic-access")
2357                         public void run() {
2358                                 Sone sone = getRemoteSone(identity.getId(), false);
2359                                 sone.setIdentity(identity);
2360                                 sone.setLatestEdition(Numbers.safeParseLong(identity.getProperty("Sone.LatestEdition"), sone.getLatestEdition()));
2361                                 soneDownloader.addSone(sone);
2362                                 soneDownloader.fetchSone(sone);
2363                         }
2364                 });
2365         }
2366
2367         /**
2368          * {@inheritDoc}
2369          */
2370         @Override
2371         public void identityRemoved(OwnIdentity ownIdentity, Identity identity) {
2372                 trustedIdentities.get(ownIdentity).remove(identity);
2373                 boolean foundIdentity = false;
2374                 for (Entry<OwnIdentity, Set<Identity>> trustedIdentity : trustedIdentities.entrySet()) {
2375                         if (trustedIdentity.getKey().equals(ownIdentity)) {
2376                                 continue;
2377                         }
2378                         if (trustedIdentity.getValue().contains(identity)) {
2379                                 foundIdentity = true;
2380                         }
2381                 }
2382                 if (foundIdentity) {
2383                         /* some local identity still trusts this identity, don’t remove. */
2384                         return;
2385                 }
2386                 Sone sone = getSone(identity.getId(), false);
2387                 if (sone == null) {
2388                         /* TODO - we don’t have the Sone anymore. should this happen? */
2389                         return;
2390                 }
2391                 synchronized (posts) {
2392                         synchronized (knownPosts) {
2393                                 for (Post post : sone.getPosts()) {
2394                                         posts.remove(post.getId());
2395                                         eventBus.post(new PostRemovedEvent(post));
2396                                 }
2397                         }
2398                 }
2399                 synchronized (replies) {
2400                         synchronized (knownReplies) {
2401                                 for (PostReply reply : sone.getReplies()) {
2402                                         replies.remove(reply.getId());
2403                                         eventBus.post(new PostReplyRemovedEvent(reply));
2404                                 }
2405                         }
2406                 }
2407                 synchronized (sones) {
2408                         sones.remove(identity.getId());
2409                 }
2410                 eventBus.post(new SoneRemovedEvent(sone));
2411         }
2412
2413         /**
2414          * Deletes the temporary image.
2415          *
2416          * @param imageInsertFinishedEvent
2417          *            The event
2418          */
2419         @Subscribe
2420         public void imageInsertFinished(ImageInsertFinishedEvent imageInsertFinishedEvent) {
2421                 logger.log(Level.WARNING, String.format("Image insert finished for %s: %s", imageInsertFinishedEvent.image(), imageInsertFinishedEvent.resultingUri()));
2422                 imageInsertFinishedEvent.image().setKey(imageInsertFinishedEvent.resultingUri().toString());
2423                 deleteTemporaryImage(imageInsertFinishedEvent.image().getId());
2424                 touchConfiguration();
2425         }
2426
2427         /**
2428          * Convenience interface for external classes that want to access the core’s
2429          * configuration.
2430          *
2431          * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
2432          */
2433         public static class Preferences {
2434
2435                 /** The wrapped options. */
2436                 private final Options options;
2437
2438                 /**
2439                  * Creates a new preferences object wrapped around the given options.
2440                  *
2441                  * @param options
2442                  *            The options to wrap
2443                  */
2444                 public Preferences(Options options) {
2445                         this.options = options;
2446                 }
2447
2448                 /**
2449                  * Returns the insertion delay.
2450                  *
2451                  * @return The insertion delay
2452                  */
2453                 public int getInsertionDelay() {
2454                         return options.getIntegerOption("InsertionDelay").get();
2455                 }
2456
2457                 /**
2458                  * Validates the given insertion delay.
2459                  *
2460                  * @param insertionDelay
2461                  *            The insertion delay to validate
2462                  * @return {@code true} if the given insertion delay was valid,
2463                  *         {@code false} otherwise
2464                  */
2465                 public boolean validateInsertionDelay(Integer insertionDelay) {
2466                         return options.getIntegerOption("InsertionDelay").validate(insertionDelay);
2467                 }
2468
2469                 /**
2470                  * Sets the insertion delay
2471                  *
2472                  * @param insertionDelay
2473                  *            The new insertion delay, or {@code null} to restore it to
2474                  *            the default value
2475                  * @return This preferences
2476                  */
2477                 public Preferences setInsertionDelay(Integer insertionDelay) {
2478                         options.getIntegerOption("InsertionDelay").set(insertionDelay);
2479                         return this;
2480                 }
2481
2482                 /**
2483                  * Returns the number of posts to show per page.
2484                  *
2485                  * @return The number of posts to show per page
2486                  */
2487                 public int getPostsPerPage() {
2488                         return options.getIntegerOption("PostsPerPage").get();
2489                 }
2490
2491                 /**
2492                  * Validates the number of posts per page.
2493                  *
2494                  * @param postsPerPage
2495                  *            The number of posts per page
2496                  * @return {@code true} if the number of posts per page was valid,
2497                  *         {@code false} otherwise
2498                  */
2499                 public boolean validatePostsPerPage(Integer postsPerPage) {
2500                         return options.getIntegerOption("PostsPerPage").validate(postsPerPage);
2501                 }
2502
2503                 /**
2504                  * Sets the number of posts to show per page.
2505                  *
2506                  * @param postsPerPage
2507                  *            The number of posts to show per page
2508                  * @return This preferences object
2509                  */
2510                 public Preferences setPostsPerPage(Integer postsPerPage) {
2511                         options.getIntegerOption("PostsPerPage").set(postsPerPage);
2512                         return this;
2513                 }
2514
2515                 /**
2516                  * Returns the number of images to show per page.
2517                  *
2518                  * @return The number of images to show per page
2519                  */
2520                 public int getImagesPerPage() {
2521                         return options.getIntegerOption("ImagesPerPage").get();
2522                 }
2523
2524                 /**
2525                  * Validates the number of images per page.
2526                  *
2527                  * @param imagesPerPage
2528                  *            The number of images per page
2529                  * @return {@code true} if the number of images per page was valid,
2530                  *         {@code false} otherwise
2531                  */
2532                 public boolean validateImagesPerPage(Integer imagesPerPage) {
2533                         return options.getIntegerOption("ImagesPerPage").validate(imagesPerPage);
2534                 }
2535
2536                 /**
2537                  * Sets the number of images per page.
2538                  *
2539                  * @param imagesPerPage
2540                  *            The number of images per page
2541                  * @return This preferences object
2542                  */
2543                 public Preferences setImagesPerPage(Integer imagesPerPage) {
2544                         options.getIntegerOption("ImagesPerPage").set(imagesPerPage);
2545                         return this;
2546                 }
2547
2548                 /**
2549                  * Returns the number of characters per post, or <code>-1</code> if the
2550                  * posts should not be cut off.
2551                  *
2552                  * @return The numbers of characters per post
2553                  */
2554                 public int getCharactersPerPost() {
2555                         return options.getIntegerOption("CharactersPerPost").get();
2556                 }
2557
2558                 /**
2559                  * Validates the number of characters per post.
2560                  *
2561                  * @param charactersPerPost
2562                  *            The number of characters per post
2563                  * @return {@code true} if the number of characters per post was valid,
2564                  *         {@code false} otherwise
2565                  */
2566                 public boolean validateCharactersPerPost(Integer charactersPerPost) {
2567                         return options.getIntegerOption("CharactersPerPost").validate(charactersPerPost);
2568                 }
2569
2570                 /**
2571                  * Sets the number of characters per post.
2572                  *
2573                  * @param charactersPerPost
2574                  *            The number of characters per post, or <code>-1</code> to
2575                  *            not cut off the posts
2576                  * @return This preferences objects
2577                  */
2578                 public Preferences setCharactersPerPost(Integer charactersPerPost) {
2579                         options.getIntegerOption("CharactersPerPost").set(charactersPerPost);
2580                         return this;
2581                 }
2582
2583                 /**
2584                  * Returns the number of characters the shortened post should have.
2585                  *
2586                  * @return The number of characters of the snippet
2587                  */
2588                 public int getPostCutOffLength() {
2589                         return options.getIntegerOption("PostCutOffLength").get();
2590                 }
2591
2592                 /**
2593                  * Validates the number of characters after which to cut off the post.
2594                  *
2595                  * @param postCutOffLength
2596                  *            The number of characters of the snippet
2597                  * @return {@code true} if the number of characters of the snippet is
2598                  *         valid, {@code false} otherwise
2599                  */
2600                 public boolean validatePostCutOffLength(Integer postCutOffLength) {
2601                         return options.getIntegerOption("PostCutOffLength").validate(postCutOffLength);
2602                 }
2603
2604                 /**
2605                  * Sets the number of characters the shortened post should have.
2606                  *
2607                  * @param postCutOffLength
2608                  *            The number of characters of the snippet
2609                  * @return This preferences
2610                  */
2611                 public Preferences setPostCutOffLength(Integer postCutOffLength) {
2612                         options.getIntegerOption("PostCutOffLength").set(postCutOffLength);
2613                         return this;
2614                 }
2615
2616                 /**
2617                  * Returns whether Sone requires full access to be even visible.
2618                  *
2619                  * @return {@code true} if Sone requires full access, {@code false}
2620                  *         otherwise
2621                  */
2622                 public boolean isRequireFullAccess() {
2623                         return options.getBooleanOption("RequireFullAccess").get();
2624                 }
2625
2626                 /**
2627                  * Sets whether Sone requires full access to be even visible.
2628                  *
2629                  * @param requireFullAccess
2630                  *            {@code true} if Sone requires full access, {@code false}
2631                  *            otherwise
2632                  */
2633                 public void setRequireFullAccess(Boolean requireFullAccess) {
2634                         options.getBooleanOption("RequireFullAccess").set(requireFullAccess);
2635                 }
2636
2637                 /**
2638                  * Returns the positive trust.
2639                  *
2640                  * @return The positive trust
2641                  */
2642                 public int getPositiveTrust() {
2643                         return options.getIntegerOption("PositiveTrust").get();
2644                 }
2645
2646                 /**
2647                  * Validates the positive trust.
2648                  *
2649                  * @param positiveTrust
2650                  *            The positive trust to validate
2651                  * @return {@code true} if the positive trust was valid, {@code false}
2652                  *         otherwise
2653                  */
2654                 public boolean validatePositiveTrust(Integer positiveTrust) {
2655                         return options.getIntegerOption("PositiveTrust").validate(positiveTrust);
2656                 }
2657
2658                 /**
2659                  * Sets the positive trust.
2660                  *
2661                  * @param positiveTrust
2662                  *            The new positive trust, or {@code null} to restore it to
2663                  *            the default vlaue
2664                  * @return This preferences
2665                  */
2666                 public Preferences setPositiveTrust(Integer positiveTrust) {
2667                         options.getIntegerOption("PositiveTrust").set(positiveTrust);
2668                         return this;
2669                 }
2670
2671                 /**
2672                  * Returns the negative trust.
2673                  *
2674                  * @return The negative trust
2675                  */
2676                 public int getNegativeTrust() {
2677                         return options.getIntegerOption("NegativeTrust").get();
2678                 }
2679
2680                 /**
2681                  * Validates the negative trust.
2682                  *
2683                  * @param negativeTrust
2684                  *            The negative trust to validate
2685                  * @return {@code true} if the negative trust was valid, {@code false}
2686                  *         otherwise
2687                  */
2688                 public boolean validateNegativeTrust(Integer negativeTrust) {
2689                         return options.getIntegerOption("NegativeTrust").validate(negativeTrust);
2690                 }
2691
2692                 /**
2693                  * Sets the negative trust.
2694                  *
2695                  * @param negativeTrust
2696                  *            The negative trust, or {@code null} to restore it to the
2697                  *            default value
2698                  * @return The preferences
2699                  */
2700                 public Preferences setNegativeTrust(Integer negativeTrust) {
2701                         options.getIntegerOption("NegativeTrust").set(negativeTrust);
2702                         return this;
2703                 }
2704
2705                 /**
2706                  * Returns the trust comment. This is the comment that is set in the web
2707                  * of trust when a trust value is assigned to an identity.
2708                  *
2709                  * @return The trust comment
2710                  */
2711                 public String getTrustComment() {
2712                         return options.getStringOption("TrustComment").get();
2713                 }
2714
2715                 /**
2716                  * Sets the trust comment.
2717                  *
2718                  * @param trustComment
2719                  *            The trust comment, or {@code null} to restore it to the
2720                  *            default value
2721                  * @return This preferences
2722                  */
2723                 public Preferences setTrustComment(String trustComment) {
2724                         options.getStringOption("TrustComment").set(trustComment);
2725                         return this;
2726                 }
2727
2728                 /**
2729                  * Returns whether the {@link FcpInterface FCP interface} is currently
2730                  * active.
2731                  *
2732                  * @see FcpInterface#setActive(boolean)
2733                  * @return {@code true} if the FCP interface is currently active,
2734                  *         {@code false} otherwise
2735                  */
2736                 public boolean isFcpInterfaceActive() {
2737                         return options.getBooleanOption("ActivateFcpInterface").get();
2738                 }
2739
2740                 /**
2741                  * Sets whether the {@link FcpInterface FCP interface} is currently
2742                  * active.
2743                  *
2744                  * @see FcpInterface#setActive(boolean)
2745                  * @param fcpInterfaceActive
2746                  *            {@code true} to activate the FCP interface, {@code false}
2747                  *            to deactivate the FCP interface
2748                  * @return This preferences object
2749                  */
2750                 public Preferences setFcpInterfaceActive(boolean fcpInterfaceActive) {
2751                         options.getBooleanOption("ActivateFcpInterface").set(fcpInterfaceActive);
2752                         return this;
2753                 }
2754
2755                 /**
2756                  * Returns the action level for which full access to the FCP interface
2757                  * is required.
2758                  *
2759                  * @return The action level for which full access to the FCP interface
2760                  *         is required
2761                  */
2762                 public FullAccessRequired getFcpFullAccessRequired() {
2763                         return FullAccessRequired.values()[options.getIntegerOption("FcpFullAccessRequired").get()];
2764                 }
2765
2766                 /**
2767                  * Sets the action level for which full access to the FCP interface is
2768                  * required
2769                  *
2770                  * @param fcpFullAccessRequired
2771                  *            The action level
2772                  * @return This preferences
2773                  */
2774                 public Preferences setFcpFullAccessRequired(FullAccessRequired fcpFullAccessRequired) {
2775                         options.getIntegerOption("FcpFullAccessRequired").set((fcpFullAccessRequired != null) ? fcpFullAccessRequired.ordinal() : null);
2776                         return this;
2777                 }
2778
2779         }
2780
2781 }