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