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