Store version of configuration in the configuration.
[Sone.git] / src / main / java / net / pterodactylus / sone / core / Core.java
1 /*
2  * Sone - Core.java - Copyright © 2010 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.Collections;
23 import java.util.HashMap;
24 import java.util.HashSet;
25 import java.util.List;
26 import java.util.Map;
27 import java.util.Set;
28 import java.util.logging.Level;
29 import java.util.logging.Logger;
30
31 import net.pterodactylus.sone.core.Options.DefaultOption;
32 import net.pterodactylus.sone.core.Options.Option;
33 import net.pterodactylus.sone.core.Options.OptionWatcher;
34 import net.pterodactylus.sone.data.Client;
35 import net.pterodactylus.sone.data.Post;
36 import net.pterodactylus.sone.data.Profile;
37 import net.pterodactylus.sone.data.Reply;
38 import net.pterodactylus.sone.data.Sone;
39 import net.pterodactylus.sone.freenet.wot.Identity;
40 import net.pterodactylus.sone.freenet.wot.IdentityListener;
41 import net.pterodactylus.sone.freenet.wot.IdentityManager;
42 import net.pterodactylus.sone.freenet.wot.OwnIdentity;
43 import net.pterodactylus.sone.main.SonePlugin;
44 import net.pterodactylus.util.config.Configuration;
45 import net.pterodactylus.util.config.ConfigurationException;
46 import net.pterodactylus.util.logging.Logging;
47 import net.pterodactylus.util.number.Numbers;
48 import freenet.keys.FreenetURI;
49
50 /**
51  * The Sone core.
52  *
53  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
54  */
55 public class Core implements IdentityListener {
56
57         /**
58          * Enumeration for the possible states of a {@link Sone}.
59          *
60          * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
61          */
62         public enum SoneStatus {
63
64                 /** The Sone is unknown, i.e. not yet downloaded. */
65                 unknown,
66
67                 /** The Sone is idle, i.e. not being downloaded or inserted. */
68                 idle,
69
70                 /** The Sone is currently being inserted. */
71                 inserting,
72
73                 /** The Sone is currently being downloaded. */
74                 downloading,
75         }
76
77         /** The logger. */
78         private static final Logger logger = Logging.getLogger(Core.class);
79
80         /** The options. */
81         private final Options options = new Options();
82
83         /** The core listener manager. */
84         private final CoreListenerManager coreListenerManager = new CoreListenerManager(this);
85
86         /** The configuration. */
87         private Configuration configuration;
88
89         /** Whether we’re currently saving the configuration. */
90         private boolean storingConfiguration = false;
91
92         /** The identity manager. */
93         private final IdentityManager identityManager;
94
95         /** Interface to freenet. */
96         private final FreenetInterface freenetInterface;
97
98         /** The Sone downloader. */
99         private final SoneDownloader soneDownloader;
100
101         /** Whether the core has been stopped. */
102         private volatile boolean stopped;
103
104         /** The Sones’ statuses. */
105         /* synchronize access on itself. */
106         private final Map<Sone, SoneStatus> soneStatuses = new HashMap<Sone, SoneStatus>();
107
108         /** Locked local Sones. */
109         /* synchronize on itself. */
110         private final Set<Sone> lockedSones = new HashSet<Sone>();
111
112         /** Sone inserters. */
113         /* synchronize access on this on localSones. */
114         private final Map<Sone, SoneInserter> soneInserters = new HashMap<Sone, SoneInserter>();
115
116         /** All local Sones. */
117         /* synchronize access on this on itself. */
118         private Map<String, Sone> localSones = new HashMap<String, Sone>();
119
120         /** All remote Sones. */
121         /* synchronize access on this on itself. */
122         private Map<String, Sone> remoteSones = new HashMap<String, Sone>();
123
124         /** All new Sones. */
125         private Set<String> newSones = new HashSet<String>();
126
127         /** All known Sones. */
128         /* synchronize access on {@link #newSones}. */
129         private Set<String> knownSones = new HashSet<String>();
130
131         /** All posts. */
132         private Map<String, Post> posts = new HashMap<String, Post>();
133
134         /** All new posts. */
135         private Set<String> newPosts = new HashSet<String>();
136
137         /** All known posts. */
138         /* synchronize access on {@link #newPosts}. */
139         private Set<String> knownPosts = new HashSet<String>();
140
141         /** All replies. */
142         private Map<String, Reply> replies = new HashMap<String, Reply>();
143
144         /** All new replies. */
145         private Set<String> newReplies = new HashSet<String>();
146
147         /** All known replies. */
148         private Set<String> knownReplies = new HashSet<String>();
149
150         /**
151          * Creates a new core.
152          *
153          * @param configuration
154          *            The configuration of the core
155          * @param freenetInterface
156          *            The freenet interface
157          * @param identityManager
158          *            The identity manager
159          */
160         public Core(Configuration configuration, FreenetInterface freenetInterface, IdentityManager identityManager) {
161                 this.configuration = configuration;
162                 this.freenetInterface = freenetInterface;
163                 this.identityManager = identityManager;
164                 this.soneDownloader = new SoneDownloader(this, freenetInterface);
165         }
166
167         //
168         // LISTENER MANAGEMENT
169         //
170
171         /**
172          * Adds a new core listener.
173          *
174          * @param coreListener
175          *            The listener to add
176          */
177         public void addCoreListener(CoreListener coreListener) {
178                 coreListenerManager.addListener(coreListener);
179         }
180
181         /**
182          * Removes a core listener.
183          *
184          * @param coreListener
185          *            The listener to remove
186          */
187         public void removeCoreListener(CoreListener coreListener) {
188                 coreListenerManager.removeListener(coreListener);
189         }
190
191         //
192         // ACCESSORS
193         //
194
195         /**
196          * Sets the configuration to use. This will automatically save the current
197          * configuration to the given configuration.
198          *
199          * @param configuration
200          *            The new configuration to use
201          */
202         public void setConfiguration(Configuration configuration) {
203                 this.configuration = configuration;
204                 saveConfiguration();
205         }
206
207         /**
208          * Returns the options used by the core.
209          *
210          * @return The options of the core
211          */
212         public Options getOptions() {
213                 return options;
214         }
215
216         /**
217          * Returns whether the “Sone rescue mode” is currently activated.
218          *
219          * @return {@code true} if the “Sone rescue mode” is currently activated,
220          *         {@code false} if it is not
221          */
222         public boolean isSoneRescueMode() {
223                 return options.getBooleanOption("SoneRescueMode").get();
224         }
225
226         /**
227          * Returns the identity manager used by the core.
228          *
229          * @return The identity manager
230          */
231         public IdentityManager getIdentityManager() {
232                 return identityManager;
233         }
234
235         /**
236          * Returns the status of the given Sone.
237          *
238          * @param sone
239          *            The Sone to get the status for
240          * @return The status of the Sone
241          */
242         public SoneStatus getSoneStatus(Sone sone) {
243                 synchronized (soneStatuses) {
244                         return soneStatuses.get(sone);
245                 }
246         }
247
248         /**
249          * Sets the status of the given Sone.
250          *
251          * @param sone
252          *            The Sone to set the status of
253          * @param soneStatus
254          *            The status to set
255          */
256         public void setSoneStatus(Sone sone, SoneStatus soneStatus) {
257                 synchronized (soneStatuses) {
258                         soneStatuses.put(sone, soneStatus);
259                 }
260         }
261
262         /**
263          * Returns whether the given Sone is currently locked.
264          *
265          * @param sone
266          *            The sone to check
267          * @return {@code true} if the Sone is locked, {@code false} if it is not
268          */
269         public boolean isLocked(Sone sone) {
270                 synchronized (lockedSones) {
271                         return lockedSones.contains(sone);
272                 }
273         }
274
275         /**
276          * Returns all Sones, remote and local.
277          *
278          * @return All Sones
279          */
280         public Set<Sone> getSones() {
281                 Set<Sone> allSones = new HashSet<Sone>();
282                 allSones.addAll(getLocalSones());
283                 allSones.addAll(getRemoteSones());
284                 return allSones;
285         }
286
287         /**
288          * Returns the Sone with the given ID, regardless whether it’s local or
289          * remote.
290          *
291          * @param id
292          *            The ID of the Sone to get
293          * @return The Sone with the given ID, or {@code null} if there is no such
294          *         Sone
295          */
296         public Sone getSone(String id) {
297                 return getSone(id, true);
298         }
299
300         /**
301          * Returns the Sone with the given ID, regardless whether it’s local or
302          * remote.
303          *
304          * @param id
305          *            The ID of the Sone to get
306          * @param create
307          *            {@code true} to create a new Sone if none exists,
308          *            {@code false} to return {@code null} if a Sone with the given
309          *            ID does not exist
310          * @return The Sone with the given ID, or {@code null} if there is no such
311          *         Sone
312          */
313         public Sone getSone(String id, boolean create) {
314                 if (isLocalSone(id)) {
315                         return getLocalSone(id);
316                 }
317                 return getRemoteSone(id, create);
318         }
319
320         /**
321          * Checks whether the core knows a Sone with the given ID.
322          *
323          * @param id
324          *            The ID of the Sone
325          * @return {@code true} if there is a Sone with the given ID, {@code false}
326          *         otherwise
327          */
328         public boolean hasSone(String id) {
329                 return isLocalSone(id) || isRemoteSone(id);
330         }
331
332         /**
333          * Returns whether the given Sone is a local Sone.
334          *
335          * @param sone
336          *            The Sone to check for its locality
337          * @return {@code true} if the given Sone is local, {@code false} otherwise
338          */
339         public boolean isLocalSone(Sone sone) {
340                 synchronized (localSones) {
341                         return localSones.containsKey(sone.getId());
342                 }
343         }
344
345         /**
346          * Returns whether the given ID is the ID of a local Sone.
347          *
348          * @param id
349          *            The Sone ID to check for its locality
350          * @return {@code true} if the given ID is a local Sone, {@code false}
351          *         otherwise
352          */
353         public boolean isLocalSone(String id) {
354                 synchronized (localSones) {
355                         return localSones.containsKey(id);
356                 }
357         }
358
359         /**
360          * Returns all local Sones.
361          *
362          * @return All local Sones
363          */
364         public Set<Sone> getLocalSones() {
365                 synchronized (localSones) {
366                         return new HashSet<Sone>(localSones.values());
367                 }
368         }
369
370         /**
371          * Returns the local Sone with the given ID.
372          *
373          * @param id
374          *            The ID of the Sone to get
375          * @return The Sone with the given ID
376          */
377         public Sone getLocalSone(String id) {
378                 return getLocalSone(id, true);
379         }
380
381         /**
382          * Returns the local Sone with the given ID, optionally creating a new Sone.
383          *
384          * @param id
385          *            The ID of the Sone
386          * @param create
387          *            {@code true} to create a new Sone if none exists,
388          *            {@code false} to return null if none exists
389          * @return The Sone with the given ID, or {@code null}
390          */
391         public Sone getLocalSone(String id, boolean create) {
392                 synchronized (localSones) {
393                         Sone sone = localSones.get(id);
394                         if ((sone == null) && create) {
395                                 sone = new Sone(id);
396                                 localSones.put(id, sone);
397                         }
398                         return sone;
399                 }
400         }
401
402         /**
403          * Returns all remote Sones.
404          *
405          * @return All remote Sones
406          */
407         public Set<Sone> getRemoteSones() {
408                 synchronized (remoteSones) {
409                         return new HashSet<Sone>(remoteSones.values());
410                 }
411         }
412
413         /**
414          * Returns the remote Sone with the given ID.
415          *
416          * @param id
417          *            The ID of the remote Sone to get
418          * @return The Sone with the given ID
419          */
420         public Sone getRemoteSone(String id) {
421                 return getRemoteSone(id, true);
422         }
423
424         /**
425          * Returns the remote Sone with the given ID.
426          *
427          * @param id
428          *            The ID of the remote Sone to get
429          * @param create
430          *            {@code true} to always create a Sone, {@code false} to return
431          *            {@code null} if no Sone with the given ID exists
432          * @return The Sone with the given ID
433          */
434         public Sone getRemoteSone(String id, boolean create) {
435                 synchronized (remoteSones) {
436                         Sone sone = remoteSones.get(id);
437                         if ((sone == null) && create) {
438                                 sone = new Sone(id);
439                                 remoteSones.put(id, sone);
440                         }
441                         return sone;
442                 }
443         }
444
445         /**
446          * Returns whether the given Sone is a remote Sone.
447          *
448          * @param sone
449          *            The Sone to check
450          * @return {@code true} if the given Sone is a remote Sone, {@code false}
451          *         otherwise
452          */
453         public boolean isRemoteSone(Sone sone) {
454                 synchronized (remoteSones) {
455                         return remoteSones.containsKey(sone.getId());
456                 }
457         }
458
459         /**
460          * Returns whether the Sone with the given ID is a remote Sone.
461          *
462          * @param id
463          *            The ID of the Sone to check
464          * @return {@code true} if the Sone with the given ID is a remote Sone,
465          *         {@code false} otherwise
466          */
467         public boolean isRemoteSone(String id) {
468                 synchronized (remoteSones) {
469                         return remoteSones.containsKey(id);
470                 }
471         }
472
473         /**
474          * Returns whether the given Sone is a new Sone. After this check, the Sone
475          * is marked as known, i.e. a second call with the same parameters will
476          * always yield {@code false}.
477          *
478          * @param sone
479          *            The sone to check for
480          * @return {@code true} if the given Sone is new, false otherwise
481          */
482         public boolean isNewSone(Sone sone) {
483                 synchronized (newSones) {
484                         boolean isNew = !knownSones.contains(sone.getId()) && newSones.remove(sone.getId());
485                         knownSones.add(sone.getId());
486                         if (isNew) {
487                                 coreListenerManager.fireMarkSoneKnown(sone);
488                         }
489                         return isNew;
490                 }
491         }
492
493         /**
494          * Returns whether the given Sone has been modified.
495          *
496          * @param sone
497          *            The Sone to check for modifications
498          * @return {@code true} if a modification has been detected in the Sone,
499          *         {@code false} otherwise
500          */
501         public boolean isModifiedSone(Sone sone) {
502                 return (soneInserters.containsKey(sone)) ? soneInserters.get(sone).isModified() : false;
503         }
504
505         /**
506          * Returns the post with the given ID.
507          *
508          * @param postId
509          *            The ID of the post to get
510          * @return The post, or {@code null} if there is no such post
511          */
512         public Post getPost(String postId) {
513                 return getPost(postId, true);
514         }
515
516         /**
517          * Returns the post with the given ID, optionally creating a new post.
518          *
519          * @param postId
520          *            The ID of the post to get
521          * @param create
522          *            {@code true} it create a new post if no post with the given ID
523          *            exists, {@code false} to return {@code null}
524          * @return The post, or {@code null} if there is no such post
525          */
526         public Post getPost(String postId, boolean create) {
527                 synchronized (posts) {
528                         Post post = posts.get(postId);
529                         if ((post == null) && create) {
530                                 post = new Post(postId);
531                                 posts.put(postId, post);
532                         }
533                         return post;
534                 }
535         }
536
537         /**
538          * Returns whether the given post ID is new. After this method returns it is
539          * marked a known post ID.
540          *
541          * @param postId
542          *            The post ID
543          * @return {@code true} if the post is considered to be new, {@code false}
544          *         otherwise
545          */
546         public boolean isNewPost(String postId) {
547                 return isNewPost(postId, true);
548         }
549
550         /**
551          * Returns whether the given post ID is new. If {@code markAsKnown} is
552          * {@code true} then after this method returns the post ID is marked a known
553          * post ID.
554          *
555          * @param postId
556          *            The post ID
557          * @param markAsKnown
558          *            {@code true} to mark the post ID as known, {@code false} to
559          *            not to mark it as known
560          * @return {@code true} if the post is considered to be new, {@code false}
561          *         otherwise
562          */
563         public boolean isNewPost(String postId, boolean markAsKnown) {
564                 synchronized (newPosts) {
565                         boolean isNew = !knownPosts.contains(postId) && newPosts.contains(postId);
566                         if (markAsKnown) {
567                                 Post post = getPost(postId, false);
568                                 if (post != null) {
569                                         markPostKnown(post);
570                                 }
571                         }
572                         return isNew;
573                 }
574         }
575
576         /**
577          * Returns the reply with the given ID. If there is no reply with the given
578          * ID yet, a new one is created.
579          *
580          * @param replyId
581          *            The ID of the reply to get
582          * @return The reply
583          */
584         public Reply getReply(String replyId) {
585                 return getReply(replyId, true);
586         }
587
588         /**
589          * Returns the reply with the given ID. If there is no reply with the given
590          * ID yet, a new one is created, unless {@code create} is false in which
591          * case {@code null} is returned.
592          *
593          * @param replyId
594          *            The ID of the reply to get
595          * @param create
596          *            {@code true} to always return a {@link Reply}, {@code false}
597          *            to return {@code null} if no reply can be found
598          * @return The reply, or {@code null} if there is no such reply
599          */
600         public Reply getReply(String replyId, boolean create) {
601                 synchronized (replies) {
602                         Reply reply = replies.get(replyId);
603                         if (create && (reply == null)) {
604                                 reply = new Reply(replyId);
605                                 replies.put(replyId, reply);
606                         }
607                         return reply;
608                 }
609         }
610
611         /**
612          * Returns all replies for the given post, order ascending by time.
613          *
614          * @param post
615          *            The post to get all replies for
616          * @return All replies for the given post
617          */
618         public List<Reply> getReplies(Post post) {
619                 Set<Sone> sones = getSones();
620                 List<Reply> replies = new ArrayList<Reply>();
621                 for (Sone sone : sones) {
622                         for (Reply reply : sone.getReplies()) {
623                                 if (reply.getPost().equals(post)) {
624                                         replies.add(reply);
625                                 }
626                         }
627                 }
628                 Collections.sort(replies, Reply.TIME_COMPARATOR);
629                 return replies;
630         }
631
632         /**
633          * Returns whether the reply with the given ID is new.
634          *
635          * @param replyId
636          *            The ID of the reply to check
637          * @return {@code true} if the reply is considered to be new, {@code false}
638          *         otherwise
639          */
640         public boolean isNewReply(String replyId) {
641                 return isNewReply(replyId, true);
642         }
643
644         /**
645          * Returns whether the reply with the given ID is new.
646          *
647          * @param replyId
648          *            The ID of the reply to check
649          * @param markAsKnown
650          *            {@code true} to mark the reply as known, {@code false} to not
651          *            to mark it as known
652          * @return {@code true} if the reply is considered to be new, {@code false}
653          *         otherwise
654          */
655         public boolean isNewReply(String replyId, boolean markAsKnown) {
656                 synchronized (newReplies) {
657                         boolean isNew = !knownReplies.contains(replyId) && newReplies.contains(replyId);
658                         if (markAsKnown) {
659                                 Reply reply = getReply(replyId, false);
660                                 if (reply != null) {
661                                         markReplyKnown(reply);
662                                 }
663                         }
664                         return isNew;
665                 }
666         }
667
668         /**
669          * Returns all Sones that have liked the given post.
670          *
671          * @param post
672          *            The post to get the liking Sones for
673          * @return The Sones that like the given post
674          */
675         public Set<Sone> getLikes(Post post) {
676                 Set<Sone> sones = new HashSet<Sone>();
677                 for (Sone sone : getSones()) {
678                         if (sone.getLikedPostIds().contains(post.getId())) {
679                                 sones.add(sone);
680                         }
681                 }
682                 return sones;
683         }
684
685         /**
686          * Returns all Sones that have liked the given reply.
687          *
688          * @param reply
689          *            The reply to get the liking Sones for
690          * @return The Sones that like the given reply
691          */
692         public Set<Sone> getLikes(Reply reply) {
693                 Set<Sone> sones = new HashSet<Sone>();
694                 for (Sone sone : getSones()) {
695                         if (sone.getLikedReplyIds().contains(reply.getId())) {
696                                 sones.add(sone);
697                         }
698                 }
699                 return sones;
700         }
701
702         //
703         // ACTIONS
704         //
705
706         /**
707          * Locks the given Sone. A locked Sone will not be inserted by
708          * {@link SoneInserter} until it is {@link #unlockSone(Sone) unlocked}
709          * again.
710          *
711          * @param sone
712          *            The sone to lock
713          */
714         public void lockSone(Sone sone) {
715                 synchronized (lockedSones) {
716                         if (lockedSones.add(sone)) {
717                                 coreListenerManager.fireSoneLocked(sone);
718                         }
719                 }
720         }
721
722         /**
723          * Unlocks the given Sone.
724          *
725          * @see #lockSone(Sone)
726          * @param sone
727          *            The sone to unlock
728          */
729         public void unlockSone(Sone sone) {
730                 synchronized (lockedSones) {
731                         if (lockedSones.remove(sone)) {
732                                 coreListenerManager.fireSoneUnlocked(sone);
733                         }
734                 }
735         }
736
737         /**
738          * Adds a local Sone from the given ID which has to be the ID of an own
739          * identity.
740          *
741          * @param id
742          *            The ID of an own identity to add a Sone for
743          * @return The added (or already existing) Sone
744          */
745         public Sone addLocalSone(String id) {
746                 synchronized (localSones) {
747                         if (localSones.containsKey(id)) {
748                                 logger.log(Level.FINE, "Tried to add known local Sone: %s", id);
749                                 return localSones.get(id);
750                         }
751                         OwnIdentity ownIdentity = identityManager.getOwnIdentity(id);
752                         if (ownIdentity == null) {
753                                 logger.log(Level.INFO, "Invalid Sone ID: %s", id);
754                                 return null;
755                         }
756                         return addLocalSone(ownIdentity);
757                 }
758         }
759
760         /**
761          * Adds a local Sone from the given own identity.
762          *
763          * @param ownIdentity
764          *            The own identity to create a Sone from
765          * @return The added (or already existing) Sone
766          */
767         public Sone addLocalSone(OwnIdentity ownIdentity) {
768                 if (ownIdentity == null) {
769                         logger.log(Level.WARNING, "Given OwnIdentity is null!");
770                         return null;
771                 }
772                 synchronized (localSones) {
773                         final Sone sone;
774                         try {
775                                 sone = getLocalSone(ownIdentity.getId()).setIdentity(ownIdentity).setInsertUri(new FreenetURI(ownIdentity.getInsertUri())).setRequestUri(new FreenetURI(ownIdentity.getRequestUri()));
776                         } catch (MalformedURLException mue1) {
777                                 logger.log(Level.SEVERE, "Could not convert the Identity’s URIs to Freenet URIs: " + ownIdentity.getInsertUri() + ", " + ownIdentity.getRequestUri(), mue1);
778                                 return null;
779                         }
780                         sone.setLatestEdition(Numbers.safeParseLong(ownIdentity.getProperty("Sone.LatestEdition"), (long) 0));
781                         sone.setClient(new Client("Sone", SonePlugin.VERSION.toString()));
782                         /* TODO - load posts ’n stuff */
783                         localSones.put(ownIdentity.getId(), sone);
784                         final SoneInserter soneInserter = new SoneInserter(this, freenetInterface, sone);
785                         soneInserters.put(sone, soneInserter);
786                         setSoneStatus(sone, SoneStatus.idle);
787                         loadSone(sone);
788                         if (!isSoneRescueMode()) {
789                                 soneInserter.start();
790                         }
791                         new Thread(new Runnable() {
792
793                                 @Override
794                                 @SuppressWarnings("synthetic-access")
795                                 public void run() {
796                                         if (!isSoneRescueMode()) {
797                                                 soneDownloader.fetchSone(sone);
798                                                 return;
799                                         }
800                                         logger.log(Level.INFO, "Trying to restore Sone from Freenet…");
801                                         coreListenerManager.fireRescuingSone(sone);
802                                         lockSone(sone);
803                                         long edition = sone.getLatestEdition();
804                                         while (!stopped && (edition >= 0) && isSoneRescueMode()) {
805                                                 logger.log(Level.FINE, "Downloading edition " + edition + "…");
806                                                 soneDownloader.fetchSone(sone, sone.getRequestUri().setKeyType("SSK").setDocName("Sone-" + edition));
807                                                 --edition;
808                                         }
809                                         logger.log(Level.INFO, "Finished restoring Sone from Freenet, starting Inserter…");
810                                         saveSone(sone);
811                                         coreListenerManager.fireRescuedSone(sone);
812                                         soneInserter.start();
813                                 }
814
815                         }, "Sone Downloader").start();
816                         return sone;
817                 }
818         }
819
820         /**
821          * Creates a new Sone for the given own identity.
822          *
823          * @param ownIdentity
824          *            The own identity to create a Sone for
825          * @return The created Sone
826          */
827         public Sone createSone(OwnIdentity ownIdentity) {
828                 identityManager.addContext(ownIdentity, "Sone");
829                 Sone sone = addLocalSone(ownIdentity);
830                 return sone;
831         }
832
833         /**
834          * Adds the Sone of the given identity.
835          *
836          * @param identity
837          *            The identity whose Sone to add
838          * @return The added or already existing Sone
839          */
840         public Sone addRemoteSone(Identity identity) {
841                 if (identity == null) {
842                         logger.log(Level.WARNING, "Given Identity is null!");
843                         return null;
844                 }
845                 synchronized (remoteSones) {
846                         final Sone sone = getRemoteSone(identity.getId()).setIdentity(identity);
847                         boolean newSone = sone.getRequestUri() == null;
848                         sone.setRequestUri(getSoneUri(identity.getRequestUri()));
849                         sone.setLatestEdition(Numbers.safeParseLong(identity.getProperty("Sone.LatestEdition"), (long) 0));
850                         if (newSone) {
851                                 synchronized (newSones) {
852                                         newSone = !knownSones.contains(sone.getId());
853                                         if (newSone) {
854                                                 newSones.add(sone.getId());
855                                         }
856                                 }
857                                 if (newSone) {
858                                         coreListenerManager.fireNewSoneFound(sone);
859                                 }
860                         }
861                         remoteSones.put(identity.getId(), sone);
862                         soneDownloader.addSone(sone);
863                         setSoneStatus(sone, SoneStatus.unknown);
864                         new Thread(new Runnable() {
865
866                                 @Override
867                                 @SuppressWarnings("synthetic-access")
868                                 public void run() {
869                                         soneDownloader.fetchSone(sone);
870                                 }
871
872                         }, "Sone Downloader").start();
873                         return sone;
874                 }
875         }
876
877         /**
878          * Updates the stores Sone with the given Sone.
879          *
880          * @param sone
881          *            The updated Sone
882          */
883         public void updateSone(Sone sone) {
884                 if (hasSone(sone.getId())) {
885                         boolean soneRescueMode = isLocalSone(sone) && isSoneRescueMode();
886                         Sone storedSone = getSone(sone.getId());
887                         if (!soneRescueMode && !(sone.getTime() > storedSone.getTime())) {
888                                 logger.log(Level.FINE, "Downloaded Sone %s is not newer than stored Sone %s.", new Object[] { sone, storedSone });
889                                 return;
890                         }
891                         synchronized (posts) {
892                                 if (!soneRescueMode) {
893                                         for (Post post : storedSone.getPosts()) {
894                                                 posts.remove(post.getId());
895                                                 if (!sone.getPosts().contains(post)) {
896                                                         coreListenerManager.firePostRemoved(post);
897                                                 }
898                                         }
899                                 }
900                                 synchronized (newPosts) {
901                                         for (Post post : sone.getPosts()) {
902                                                 post.setSone(getSone(post.getSone().getId()));
903                                                 if (!storedSone.getPosts().contains(post) && !knownPosts.contains(post.getId())) {
904                                                         newPosts.add(post.getId());
905                                                         coreListenerManager.fireNewPostFound(post);
906                                                 }
907                                                 posts.put(post.getId(), post);
908                                         }
909                                 }
910                         }
911                         synchronized (replies) {
912                                 if (!soneRescueMode) {
913                                         for (Reply reply : storedSone.getReplies()) {
914                                                 replies.remove(reply.getId());
915                                                 if (!sone.getReplies().contains(reply)) {
916                                                         coreListenerManager.fireReplyRemoved(reply);
917                                                 }
918                                         }
919                                 }
920                                 synchronized (newReplies) {
921                                         for (Reply reply : sone.getReplies()) {
922                                                 reply.setSone(getSone(reply.getSone().getId()));
923                                                 if (!storedSone.getReplies().contains(reply) && !knownReplies.contains(reply.getId())) {
924                                                         newReplies.add(reply.getId());
925                                                         coreListenerManager.fireNewReplyFound(reply);
926                                                 }
927                                                 replies.put(reply.getId(), reply);
928                                         }
929                                 }
930                         }
931                         synchronized (storedSone) {
932                                 if (!soneRescueMode || (sone.getTime() > storedSone.getTime())) {
933                                         storedSone.setTime(sone.getTime());
934                                 }
935                                 storedSone.setClient(sone.getClient());
936                                 storedSone.setProfile(sone.getProfile());
937                                 if (soneRescueMode) {
938                                         for (Post post : sone.getPosts()) {
939                                                 storedSone.addPost(post);
940                                         }
941                                         for (Reply reply : sone.getReplies()) {
942                                                 storedSone.addReply(reply);
943                                         }
944                                         for (String likedPostId : sone.getLikedPostIds()) {
945                                                 storedSone.addLikedPostId(likedPostId);
946                                         }
947                                         for (String likedReplyId : sone.getLikedReplyIds()) {
948                                                 storedSone.addLikedReplyId(likedReplyId);
949                                         }
950                                 } else {
951                                         storedSone.setPosts(sone.getPosts());
952                                         storedSone.setReplies(sone.getReplies());
953                                         storedSone.setLikePostIds(sone.getLikedPostIds());
954                                         storedSone.setLikeReplyIds(sone.getLikedReplyIds());
955                                 }
956                                 storedSone.setLatestEdition(sone.getLatestEdition());
957                         }
958                 }
959         }
960
961         /**
962          * Deletes the given Sone. This will remove the Sone from the
963          * {@link #getLocalSone(String) local Sones}, stops its {@link SoneInserter}
964          * and remove the context from its identity.
965          *
966          * @param sone
967          *            The Sone to delete
968          */
969         public void deleteSone(Sone sone) {
970                 if (!(sone.getIdentity() instanceof OwnIdentity)) {
971                         logger.log(Level.WARNING, "Tried to delete Sone of non-own identity: %s", sone);
972                         return;
973                 }
974                 synchronized (localSones) {
975                         if (!localSones.containsKey(sone.getId())) {
976                                 logger.log(Level.WARNING, "Tried to delete non-local Sone: %s", sone);
977                                 return;
978                         }
979                         localSones.remove(sone.getId());
980                         soneInserters.remove(sone).stop();
981                 }
982                 identityManager.removeContext((OwnIdentity) sone.getIdentity(), "Sone");
983                 identityManager.removeProperty((OwnIdentity) sone.getIdentity(), "Sone.LatestEdition");
984                 try {
985                         configuration.getLongValue("Sone/" + sone.getId() + "/Time").setValue(null);
986                 } catch (ConfigurationException ce1) {
987                         logger.log(Level.WARNING, "Could not remove Sone from configuration!", ce1);
988                 }
989         }
990
991         /**
992          * Loads and updates the given Sone from the configuration. If any error is
993          * encountered, loading is aborted and the given Sone is not changed.
994          *
995          * @param sone
996          *            The Sone to load and update
997          */
998         public void loadSone(Sone sone) {
999                 if (!isLocalSone(sone)) {
1000                         logger.log(Level.FINE, "Tried to load non-local Sone: %s", sone);
1001                         return;
1002                 }
1003
1004                 /* load Sone. */
1005                 String sonePrefix = "Sone/" + sone.getId();
1006                 Long soneTime = configuration.getLongValue(sonePrefix + "/Time").getValue(null);
1007                 if (soneTime == null) {
1008                         logger.log(Level.INFO, "Could not load Sone because no Sone has been saved.");
1009                         return;
1010                 }
1011                 String lastInsertFingerprint = configuration.getStringValue(sonePrefix + "/LastInsertFingerprint").getValue("");
1012
1013                 /* load profile. */
1014                 Profile profile = new Profile();
1015                 profile.setFirstName(configuration.getStringValue(sonePrefix + "/Profile/FirstName").getValue(null));
1016                 profile.setMiddleName(configuration.getStringValue(sonePrefix + "/Profile/MiddleName").getValue(null));
1017                 profile.setLastName(configuration.getStringValue(sonePrefix + "/Profile/LastName").getValue(null));
1018                 profile.setBirthDay(configuration.getIntValue(sonePrefix + "/Profile/BirthDay").getValue(null));
1019                 profile.setBirthMonth(configuration.getIntValue(sonePrefix + "/Profile/BirthMonth").getValue(null));
1020                 profile.setBirthYear(configuration.getIntValue(sonePrefix + "/Profile/BirthYear").getValue(null));
1021
1022                 /* load posts. */
1023                 Set<Post> posts = new HashSet<Post>();
1024                 while (true) {
1025                         String postPrefix = sonePrefix + "/Posts/" + posts.size();
1026                         String postId = configuration.getStringValue(postPrefix + "/ID").getValue(null);
1027                         if (postId == null) {
1028                                 break;
1029                         }
1030                         String postRecipientId = configuration.getStringValue(postPrefix + "/Recipient").getValue(null);
1031                         long postTime = configuration.getLongValue(postPrefix + "/Time").getValue((long) 0);
1032                         String postText = configuration.getStringValue(postPrefix + "/Text").getValue(null);
1033                         if ((postTime == 0) || (postText == null)) {
1034                                 logger.log(Level.WARNING, "Invalid post found, aborting load!");
1035                                 return;
1036                         }
1037                         Post post = getPost(postId).setSone(sone).setTime(postTime).setText(postText);
1038                         if ((postRecipientId != null) && (postRecipientId.length() == 43)) {
1039                                 post.setRecipient(getSone(postRecipientId));
1040                         }
1041                         posts.add(post);
1042                 }
1043
1044                 /* load replies. */
1045                 Set<Reply> replies = new HashSet<Reply>();
1046                 while (true) {
1047                         String replyPrefix = sonePrefix + "/Replies/" + replies.size();
1048                         String replyId = configuration.getStringValue(replyPrefix + "/ID").getValue(null);
1049                         if (replyId == null) {
1050                                 break;
1051                         }
1052                         String postId = configuration.getStringValue(replyPrefix + "/Post/ID").getValue(null);
1053                         long replyTime = configuration.getLongValue(replyPrefix + "/Time").getValue((long) 0);
1054                         String replyText = configuration.getStringValue(replyPrefix + "/Text").getValue(null);
1055                         if ((postId == null) || (replyTime == 0) || (replyText == null)) {
1056                                 logger.log(Level.WARNING, "Invalid reply found, aborting load!");
1057                                 return;
1058                         }
1059                         replies.add(getReply(replyId).setSone(sone).setPost(getPost(postId)).setTime(replyTime).setText(replyText));
1060                 }
1061
1062                 /* load post likes. */
1063                 Set<String> likedPostIds = new HashSet<String>();
1064                 while (true) {
1065                         String likedPostId = configuration.getStringValue(sonePrefix + "/Likes/Post/" + likedPostIds.size() + "/ID").getValue(null);
1066                         if (likedPostId == null) {
1067                                 break;
1068                         }
1069                         likedPostIds.add(likedPostId);
1070                 }
1071
1072                 /* load reply likes. */
1073                 Set<String> likedReplyIds = new HashSet<String>();
1074                 while (true) {
1075                         String likedReplyId = configuration.getStringValue(sonePrefix + "/Likes/Reply/" + likedReplyIds.size() + "/ID").getValue(null);
1076                         if (likedReplyId == null) {
1077                                 break;
1078                         }
1079                         likedReplyIds.add(likedReplyId);
1080                 }
1081
1082                 /* load friends. */
1083                 Set<String> friends = new HashSet<String>();
1084                 while (true) {
1085                         String friendId = configuration.getStringValue(sonePrefix + "/Friends/" + friends.size() + "/ID").getValue(null);
1086                         if (friendId == null) {
1087                                 break;
1088                         }
1089                         friends.add(friendId);
1090                 }
1091
1092                 /* if we’re still here, Sone was loaded successfully. */
1093                 synchronized (sone) {
1094                         sone.setTime(soneTime);
1095                         sone.setProfile(profile);
1096                         sone.setPosts(posts);
1097                         sone.setReplies(replies);
1098                         sone.setLikePostIds(likedPostIds);
1099                         sone.setLikeReplyIds(likedReplyIds);
1100                         sone.setFriends(friends);
1101                         soneInserters.get(sone).setLastInsertFingerprint(lastInsertFingerprint);
1102                 }
1103                 synchronized (newSones) {
1104                         for (String friend : friends) {
1105                                 knownSones.add(friend);
1106                         }
1107                 }
1108                 synchronized (newPosts) {
1109                         for (Post post : posts) {
1110                                 knownPosts.add(post.getId());
1111                         }
1112                 }
1113                 synchronized (newReplies) {
1114                         for (Reply reply : replies) {
1115                                 knownReplies.add(reply.getId());
1116                         }
1117                 }
1118         }
1119
1120         /**
1121          * Saves the given Sone. This will persist all local settings for the given
1122          * Sone, such as the friends list and similar, private options.
1123          *
1124          * @param sone
1125          *            The Sone to save
1126          */
1127         public synchronized void saveSone(Sone sone) {
1128                 if (!isLocalSone(sone)) {
1129                         logger.log(Level.FINE, "Tried to save non-local Sone: %s", sone);
1130                         return;
1131                 }
1132                 if (!(sone.getIdentity() instanceof OwnIdentity)) {
1133                         logger.log(Level.WARNING, "Local Sone without OwnIdentity found, refusing to save: %s", sone);
1134                         return;
1135                 }
1136
1137                 logger.log(Level.INFO, "Saving Sone: %s", sone);
1138                 identityManager.setProperty((OwnIdentity) sone.getIdentity(), "Sone.LatestEdition", String.valueOf(sone.getLatestEdition()));
1139                 try {
1140                         /* save Sone into configuration. */
1141                         String sonePrefix = "Sone/" + sone.getId();
1142                         configuration.getLongValue(sonePrefix + "/Time").setValue(sone.getTime());
1143                         configuration.getStringValue(sonePrefix + "/LastInsertFingerprint").setValue(soneInserters.get(sone).getLastInsertFingerprint());
1144
1145                         /* save profile. */
1146                         Profile profile = sone.getProfile();
1147                         configuration.getStringValue(sonePrefix + "/Profile/FirstName").setValue(profile.getFirstName());
1148                         configuration.getStringValue(sonePrefix + "/Profile/MiddleName").setValue(profile.getMiddleName());
1149                         configuration.getStringValue(sonePrefix + "/Profile/LastName").setValue(profile.getLastName());
1150                         configuration.getIntValue(sonePrefix + "/Profile/BirthDay").setValue(profile.getBirthDay());
1151                         configuration.getIntValue(sonePrefix + "/Profile/BirthMonth").setValue(profile.getBirthMonth());
1152                         configuration.getIntValue(sonePrefix + "/Profile/BirthYear").setValue(profile.getBirthYear());
1153
1154                         /* save posts. */
1155                         int postCounter = 0;
1156                         for (Post post : sone.getPosts()) {
1157                                 String postPrefix = sonePrefix + "/Posts/" + postCounter++;
1158                                 configuration.getStringValue(postPrefix + "/ID").setValue(post.getId());
1159                                 configuration.getStringValue(postPrefix + "/Recipient").setValue((post.getRecipient() != null) ? post.getRecipient().getId() : null);
1160                                 configuration.getLongValue(postPrefix + "/Time").setValue(post.getTime());
1161                                 configuration.getStringValue(postPrefix + "/Text").setValue(post.getText());
1162                         }
1163                         configuration.getStringValue(sonePrefix + "/Posts/" + postCounter + "/ID").setValue(null);
1164
1165                         /* save replies. */
1166                         int replyCounter = 0;
1167                         for (Reply reply : sone.getReplies()) {
1168                                 String replyPrefix = sonePrefix + "/Replies/" + replyCounter++;
1169                                 configuration.getStringValue(replyPrefix + "/ID").setValue(reply.getId());
1170                                 configuration.getStringValue(replyPrefix + "/Post/ID").setValue(reply.getPost().getId());
1171                                 configuration.getLongValue(replyPrefix + "/Time").setValue(reply.getTime());
1172                                 configuration.getStringValue(replyPrefix + "/Text").setValue(reply.getText());
1173                         }
1174                         configuration.getStringValue(sonePrefix + "/Replies/" + replyCounter + "/ID").setValue(null);
1175
1176                         /* save post likes. */
1177                         int postLikeCounter = 0;
1178                         for (String postId : sone.getLikedPostIds()) {
1179                                 configuration.getStringValue(sonePrefix + "/Likes/Post/" + postLikeCounter++ + "/ID").setValue(postId);
1180                         }
1181                         configuration.getStringValue(sonePrefix + "/Likes/Post/" + postLikeCounter + "/ID").setValue(null);
1182
1183                         /* save reply likes. */
1184                         int replyLikeCounter = 0;
1185                         for (String replyId : sone.getLikedReplyIds()) {
1186                                 configuration.getStringValue(sonePrefix + "/Likes/Reply/" + replyLikeCounter++ + "/ID").setValue(replyId);
1187                         }
1188                         configuration.getStringValue(sonePrefix + "/Likes/Reply/" + replyLikeCounter + "/ID").setValue(null);
1189
1190                         /* save friends. */
1191                         int friendCounter = 0;
1192                         for (String friendId : sone.getFriends()) {
1193                                 configuration.getStringValue(sonePrefix + "/Friends/" + friendCounter++ + "/ID").setValue(friendId);
1194                         }
1195                         configuration.getStringValue(sonePrefix + "/Friends/" + friendCounter + "/ID").setValue(null);
1196
1197                         configuration.save();
1198                         logger.log(Level.INFO, "Sone %s saved.", sone);
1199                 } catch (ConfigurationException ce1) {
1200                         logger.log(Level.WARNING, "Could not save Sone: " + sone, ce1);
1201                 }
1202         }
1203
1204         /**
1205          * Creates a new post.
1206          *
1207          * @param sone
1208          *            The Sone that creates the post
1209          * @param text
1210          *            The text of the post
1211          * @return The created post
1212          */
1213         public Post createPost(Sone sone, String text) {
1214                 return createPost(sone, System.currentTimeMillis(), text);
1215         }
1216
1217         /**
1218          * Creates a new post.
1219          *
1220          * @param sone
1221          *            The Sone that creates the post
1222          * @param time
1223          *            The time of the post
1224          * @param text
1225          *            The text of the post
1226          * @return The created post
1227          */
1228         public Post createPost(Sone sone, long time, String text) {
1229                 return createPost(sone, null, time, text);
1230         }
1231
1232         /**
1233          * Creates a new post.
1234          *
1235          * @param sone
1236          *            The Sone that creates the post
1237          * @param recipient
1238          *            The recipient Sone, or {@code null} if this post does not have
1239          *            a recipient
1240          * @param text
1241          *            The text of the post
1242          * @return The created post
1243          */
1244         public Post createPost(Sone sone, Sone recipient, String text) {
1245                 return createPost(sone, recipient, System.currentTimeMillis(), text);
1246         }
1247
1248         /**
1249          * Creates a new post.
1250          *
1251          * @param sone
1252          *            The Sone that creates the post
1253          * @param recipient
1254          *            The recipient Sone, or {@code null} if this post does not have
1255          *            a recipient
1256          * @param time
1257          *            The time of the post
1258          * @param text
1259          *            The text of the post
1260          * @return The created post
1261          */
1262         public Post createPost(Sone sone, Sone recipient, long time, String text) {
1263                 if (!isLocalSone(sone)) {
1264                         logger.log(Level.FINE, "Tried to create post for non-local Sone: %s", sone);
1265                         return null;
1266                 }
1267                 Post post = new Post(sone, time, text);
1268                 if (recipient != null) {
1269                         post.setRecipient(recipient);
1270                 }
1271                 synchronized (posts) {
1272                         posts.put(post.getId(), post);
1273                 }
1274                 synchronized (newPosts) {
1275                         knownPosts.add(post.getId());
1276                 }
1277                 sone.addPost(post);
1278                 saveSone(sone);
1279                 return post;
1280         }
1281
1282         /**
1283          * Deletes the given post.
1284          *
1285          * @param post
1286          *            The post to delete
1287          */
1288         public void deletePost(Post post) {
1289                 if (!isLocalSone(post.getSone())) {
1290                         logger.log(Level.WARNING, "Tried to delete post of non-local Sone: %s", post.getSone());
1291                         return;
1292                 }
1293                 post.getSone().removePost(post);
1294                 synchronized (posts) {
1295                         posts.remove(post.getId());
1296                 }
1297                 saveSone(post.getSone());
1298         }
1299
1300         /**
1301          * Marks the given post as known, if it is currently a new post (according
1302          * to {@link #isNewPost(String)}).
1303          *
1304          * @param post
1305          *            The post to mark as known
1306          */
1307         public void markPostKnown(Post post) {
1308                 synchronized (newPosts) {
1309                         if (newPosts.remove(post.getId())) {
1310                                 knownPosts.add(post.getId());
1311                                 coreListenerManager.fireMarkPostKnown(post);
1312                                 saveConfiguration();
1313                         }
1314                 }
1315         }
1316
1317         /**
1318          * Creates a new reply.
1319          *
1320          * @param sone
1321          *            The Sone that creates the reply
1322          * @param post
1323          *            The post that this reply refers to
1324          * @param text
1325          *            The text of the reply
1326          * @return The created reply
1327          */
1328         public Reply createReply(Sone sone, Post post, String text) {
1329                 return createReply(sone, post, System.currentTimeMillis(), text);
1330         }
1331
1332         /**
1333          * Creates a new reply.
1334          *
1335          * @param sone
1336          *            The Sone that creates the reply
1337          * @param post
1338          *            The post that this reply refers to
1339          * @param time
1340          *            The time of the reply
1341          * @param text
1342          *            The text of the reply
1343          * @return The created reply
1344          */
1345         public Reply createReply(Sone sone, Post post, long time, String text) {
1346                 if (!isLocalSone(sone)) {
1347                         logger.log(Level.FINE, "Tried to create reply for non-local Sone: %s", sone);
1348                         return null;
1349                 }
1350                 Reply reply = new Reply(sone, post, System.currentTimeMillis(), text);
1351                 synchronized (replies) {
1352                         replies.put(reply.getId(), reply);
1353                 }
1354                 synchronized (newReplies) {
1355                         knownReplies.add(reply.getId());
1356                 }
1357                 sone.addReply(reply);
1358                 saveSone(sone);
1359                 return reply;
1360         }
1361
1362         /**
1363          * Deletes the given reply.
1364          *
1365          * @param reply
1366          *            The reply to delete
1367          */
1368         public void deleteReply(Reply reply) {
1369                 Sone sone = reply.getSone();
1370                 if (!isLocalSone(sone)) {
1371                         logger.log(Level.FINE, "Tried to delete non-local reply: %s", reply);
1372                         return;
1373                 }
1374                 synchronized (replies) {
1375                         replies.remove(reply.getId());
1376                 }
1377                 sone.removeReply(reply);
1378                 saveSone(sone);
1379         }
1380
1381         /**
1382          * Marks the given reply as known, if it is currently a new reply (according
1383          * to {@link #isNewReply(String)}).
1384          *
1385          * @param reply
1386          *            The reply to mark as known
1387          */
1388         public void markReplyKnown(Reply reply) {
1389                 synchronized (newReplies) {
1390                         if (newReplies.remove(reply.getId())) {
1391                                 knownReplies.add(reply.getId());
1392                                 coreListenerManager.fireMarkReplyKnown(reply);
1393                                 saveConfiguration();
1394                         }
1395                 }
1396         }
1397
1398         /**
1399          * Starts the core.
1400          */
1401         public void start() {
1402                 loadConfiguration();
1403         }
1404
1405         /**
1406          * Stops the core.
1407          */
1408         public void stop() {
1409                 synchronized (localSones) {
1410                         for (SoneInserter soneInserter : soneInserters.values()) {
1411                                 soneInserter.stop();
1412                         }
1413                 }
1414                 soneDownloader.stop();
1415                 saveConfiguration();
1416                 stopped = true;
1417         }
1418
1419         /**
1420          * Saves the current options.
1421          */
1422         public void saveConfiguration() {
1423                 synchronized (configuration) {
1424                         if (storingConfiguration) {
1425                                 logger.log(Level.FINE, "Already storing configuration…");
1426                                 return;
1427                         }
1428                         storingConfiguration = true;
1429                 }
1430
1431                 /* store the options first. */
1432                 try {
1433                         configuration.getIntValue("Option/ConfigurationVersion").setValue(0);
1434                         configuration.getIntValue("Option/InsertionDelay").setValue(options.getIntegerOption("InsertionDelay").getReal());
1435                         configuration.getBooleanValue("Option/SoneRescueMode").setValue(options.getBooleanOption("SoneRescueMode").getReal());
1436                         configuration.getBooleanValue("Option/ClearOnNextRestart").setValue(options.getBooleanOption("ClearOnNextRestart").getReal());
1437                         configuration.getBooleanValue("Option/ReallyClearOnNextRestart").setValue(options.getBooleanOption("ReallyClearOnNextRestart").getReal());
1438
1439                         /* save known Sones. */
1440                         int soneCounter = 0;
1441                         synchronized (newSones) {
1442                                 for (String knownSoneId : knownSones) {
1443                                         configuration.getStringValue("KnownSone/" + soneCounter++ + "/ID").setValue(knownSoneId);
1444                                 }
1445                                 configuration.getStringValue("KnownSone/" + soneCounter + "/ID").setValue(null);
1446                         }
1447
1448                         /* save known posts. */
1449                         int postCounter = 0;
1450                         synchronized (newPosts) {
1451                                 for (String knownPostId : knownPosts) {
1452                                         configuration.getStringValue("KnownPosts/" + postCounter++ + "/ID").setValue(knownPostId);
1453                                 }
1454                                 configuration.getStringValue("KnownPosts/" + postCounter + "/ID").setValue(null);
1455                         }
1456
1457                         /* save known replies. */
1458                         int replyCounter = 0;
1459                         synchronized (newReplies) {
1460                                 for (String knownReplyId : knownReplies) {
1461                                         configuration.getStringValue("KnownReplies/" + replyCounter++ + "/ID").setValue(knownReplyId);
1462                                 }
1463                                 configuration.getStringValue("KnownReplies/" + replyCounter + "/ID").setValue(null);
1464                         }
1465
1466                         /* now save it. */
1467                         configuration.save();
1468
1469                 } catch (ConfigurationException ce1) {
1470                         logger.log(Level.SEVERE, "Could not store configuration!", ce1);
1471                 } finally {
1472                         synchronized (configuration) {
1473                                 storingConfiguration = false;
1474                         }
1475                 }
1476         }
1477
1478         //
1479         // PRIVATE METHODS
1480         //
1481
1482         /**
1483          * Loads the configuration.
1484          */
1485         @SuppressWarnings("unchecked")
1486         private void loadConfiguration() {
1487                 /* create options. */
1488                 options.addIntegerOption("InsertionDelay", new DefaultOption<Integer>(60, new OptionWatcher<Integer>() {
1489
1490                         @Override
1491                         public void optionChanged(Option<Integer> option, Integer oldValue, Integer newValue) {
1492                                 SoneInserter.setInsertionDelay(newValue);
1493                         }
1494
1495                 }));
1496                 options.addBooleanOption("SoneRescueMode", new DefaultOption<Boolean>(false));
1497                 options.addBooleanOption("ClearOnNextRestart", new DefaultOption<Boolean>(false));
1498                 options.addBooleanOption("ReallyClearOnNextRestart", new DefaultOption<Boolean>(false));
1499
1500                 /* read options from configuration. */
1501                 options.getBooleanOption("ClearOnNextRestart").set(configuration.getBooleanValue("Option/ClearOnNextRestart").getValue(null));
1502                 options.getBooleanOption("ReallyClearOnNextRestart").set(configuration.getBooleanValue("Option/ReallyClearOnNextRestart").getValue(null));
1503                 boolean clearConfiguration = options.getBooleanOption("ClearOnNextRestart").get() && options.getBooleanOption("ReallyClearOnNextRestart").get();
1504                 options.getBooleanOption("ClearOnNextRestart").set(null);
1505                 options.getBooleanOption("ReallyClearOnNextRestart").set(null);
1506                 if (clearConfiguration) {
1507                         /* stop loading the configuration. */
1508                         return;
1509                 }
1510
1511                 options.getIntegerOption("InsertionDelay").set(configuration.getIntValue("Option/InsertionDelay").getValue(null));
1512                 options.getBooleanOption("SoneRescueMode").set(configuration.getBooleanValue("Option/SoneRescueMode").getValue(null));
1513
1514                 /* load known Sones. */
1515                 int soneCounter = 0;
1516                 while (true) {
1517                         String knownSoneId = configuration.getStringValue("KnownSone/" + soneCounter++ + "/ID").getValue(null);
1518                         if (knownSoneId == null) {
1519                                 break;
1520                         }
1521                         synchronized (newSones) {
1522                                 knownSones.add(knownSoneId);
1523                         }
1524                 }
1525
1526                 /* load known posts. */
1527                 int postCounter = 0;
1528                 while (true) {
1529                         String knownPostId = configuration.getStringValue("KnownPosts/" + postCounter++ + "/ID").getValue(null);
1530                         if (knownPostId == null) {
1531                                 break;
1532                         }
1533                         synchronized (newPosts) {
1534                                 knownPosts.add(knownPostId);
1535                         }
1536                 }
1537
1538                 /* load known replies. */
1539                 int replyCounter = 0;
1540                 while (true) {
1541                         String knownReplyId = configuration.getStringValue("KnownReplies/" + replyCounter++ + "/ID").getValue(null);
1542                         if (knownReplyId == null) {
1543                                 break;
1544                         }
1545                         synchronized (newReplies) {
1546                                 knownReplies.add(knownReplyId);
1547                         }
1548                 }
1549
1550         }
1551
1552         /**
1553          * Generate a Sone URI from the given URI and latest edition.
1554          *
1555          * @param uriString
1556          *            The URI to derive the Sone URI from
1557          * @return The derived URI
1558          */
1559         private FreenetURI getSoneUri(String uriString) {
1560                 try {
1561                         FreenetURI uri = new FreenetURI(uriString).setDocName("Sone").setMetaString(new String[0]);
1562                         return uri;
1563                 } catch (MalformedURLException mue1) {
1564                         logger.log(Level.WARNING, "Could not create Sone URI from URI: " + uriString, mue1);
1565                         return null;
1566                 }
1567         }
1568
1569         //
1570         // INTERFACE IdentityListener
1571         //
1572
1573         /**
1574          * {@inheritDoc}
1575          */
1576         @Override
1577         public void ownIdentityAdded(OwnIdentity ownIdentity) {
1578                 logger.log(Level.FINEST, "Adding OwnIdentity: " + ownIdentity);
1579                 if (ownIdentity.hasContext("Sone")) {
1580                         addLocalSone(ownIdentity);
1581                 }
1582         }
1583
1584         /**
1585          * {@inheritDoc}
1586          */
1587         @Override
1588         public void ownIdentityRemoved(OwnIdentity ownIdentity) {
1589                 logger.log(Level.FINEST, "Removing OwnIdentity: " + ownIdentity);
1590         }
1591
1592         /**
1593          * {@inheritDoc}
1594          */
1595         @Override
1596         public void identityAdded(Identity identity) {
1597                 logger.log(Level.FINEST, "Adding Identity: " + identity);
1598                 addRemoteSone(identity);
1599         }
1600
1601         /**
1602          * {@inheritDoc}
1603          */
1604         @Override
1605         public void identityUpdated(final Identity identity) {
1606                 new Thread(new Runnable() {
1607
1608                         @Override
1609                         @SuppressWarnings("synthetic-access")
1610                         public void run() {
1611                                 Sone sone = getRemoteSone(identity.getId());
1612                                 soneDownloader.fetchSone(sone);
1613                         }
1614                 }).start();
1615         }
1616
1617         /**
1618          * {@inheritDoc}
1619          */
1620         @Override
1621         public void identityRemoved(Identity identity) {
1622                 /* TODO */
1623         }
1624
1625 }