93891ab3e4af5613d2a820a9c8989d31f4bfee6f
[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.Post;
35 import net.pterodactylus.sone.data.Profile;
36 import net.pterodactylus.sone.data.Reply;
37 import net.pterodactylus.sone.data.Sone;
38 import net.pterodactylus.sone.freenet.wot.Identity;
39 import net.pterodactylus.sone.freenet.wot.IdentityListener;
40 import net.pterodactylus.sone.freenet.wot.IdentityManager;
41 import net.pterodactylus.sone.freenet.wot.OwnIdentity;
42 import net.pterodactylus.util.config.Configuration;
43 import net.pterodactylus.util.config.ConfigurationException;
44 import net.pterodactylus.util.logging.Logging;
45 import net.pterodactylus.util.number.Numbers;
46 import freenet.keys.FreenetURI;
47
48 /**
49  * The Sone core.
50  *
51  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
52  */
53 public class Core implements IdentityListener {
54
55         /**
56          * Enumeration for the possible states of a {@link Sone}.
57          *
58          * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
59          */
60         public enum SoneStatus {
61
62                 /** The Sone is unknown, i.e. not yet downloaded. */
63                 unknown,
64
65                 /** The Sone is idle, i.e. not being downloaded or inserted. */
66                 idle,
67
68                 /** The Sone is currently being inserted. */
69                 inserting,
70
71                 /** The Sone is currently being downloaded. */
72                 downloading,
73         }
74
75         /** The logger. */
76         private static final Logger logger = Logging.getLogger(Core.class);
77
78         /** The options. */
79         private final Options options = new Options();
80
81         /** The configuration. */
82         private final Configuration configuration;
83
84         /** The identity manager. */
85         private final IdentityManager identityManager;
86
87         /** Interface to freenet. */
88         private final FreenetInterface freenetInterface;
89
90         /** The Sone downloader. */
91         private final SoneDownloader soneDownloader;
92
93         /** The Sones’ statuses. */
94         /* synchronize access on itself. */
95         private final Map<Sone, SoneStatus> soneStatuses = new HashMap<Sone, SoneStatus>();
96
97         /** Sone inserters. */
98         /* synchronize access on this on localSones. */
99         private final Map<Sone, SoneInserter> soneInserters = new HashMap<Sone, SoneInserter>();
100
101         /** All local Sones. */
102         /* synchronize access on this on itself. */
103         private Map<String, Sone> localSones = new HashMap<String, Sone>();
104
105         /** All remote Sones. */
106         /* synchronize access on this on itself. */
107         private Map<String, Sone> remoteSones = new HashMap<String, Sone>();
108
109         /** All new Sones. */
110         private Set<Sone> newSones = new HashSet<Sone>();
111
112         /** All known Sones. */
113         /* synchronize access on {@link #newSones}. */
114         private Set<Sone> knownSones = new HashSet<Sone>();
115
116         /** All posts. */
117         private Map<String, Post> posts = new HashMap<String, Post>();
118
119         /** All replies. */
120         private Map<String, Reply> replies = new HashMap<String, Reply>();
121
122         /**
123          * Creates a new core.
124          *
125          * @param configuration
126          *            The configuration of the core
127          * @param freenetInterface
128          *            The freenet interface
129          * @param identityManager
130          *            The identity manager
131          */
132         public Core(Configuration configuration, FreenetInterface freenetInterface, IdentityManager identityManager) {
133                 this.configuration = configuration;
134                 this.freenetInterface = freenetInterface;
135                 this.identityManager = identityManager;
136                 this.soneDownloader = new SoneDownloader(this, freenetInterface);
137         }
138
139         //
140         // ACCESSORS
141         //
142
143         /**
144          * Returns the options used by the core.
145          *
146          * @return The options of the core
147          */
148         public Options getOptions() {
149                 return options;
150         }
151
152         /**
153          * Returns the identity manager used by the core.
154          *
155          * @return The identity manager
156          */
157         public IdentityManager getIdentityManager() {
158                 return identityManager;
159         }
160
161         /**
162          * Returns the status of the given Sone.
163          *
164          * @param sone
165          *            The Sone to get the status for
166          * @return The status of the Sone
167          */
168         public SoneStatus getSoneStatus(Sone sone) {
169                 synchronized (soneStatuses) {
170                         return soneStatuses.get(sone);
171                 }
172         }
173
174         /**
175          * Sets the status of the given Sone.
176          *
177          * @param sone
178          *            The Sone to set the status of
179          * @param soneStatus
180          *            The status to set
181          */
182         public void setSoneStatus(Sone sone, SoneStatus soneStatus) {
183                 synchronized (soneStatuses) {
184                         soneStatuses.put(sone, soneStatus);
185                 }
186         }
187
188         /**
189          * Returns all Sones, remote and local.
190          *
191          * @return All Sones
192          */
193         public Set<Sone> getSones() {
194                 Set<Sone> allSones = new HashSet<Sone>();
195                 allSones.addAll(getLocalSones());
196                 allSones.addAll(getRemoteSones());
197                 return allSones;
198         }
199
200         /**
201          * Returns the Sone with the given ID, regardless whether it’s local or
202          * remote.
203          *
204          * @param id
205          *            The ID of the Sone to get
206          * @return The Sone with the given ID, or {@code null} if there is no such
207          *         Sone
208          */
209         public Sone getSone(String id) {
210                 if (isLocalSone(id)) {
211                         return getLocalSone(id);
212                 }
213                 return getRemoteSone(id);
214         }
215
216         /**
217          * Returns whether the given Sone is a local Sone.
218          *
219          * @param sone
220          *            The Sone to check for its locality
221          * @return {@code true} if the given Sone is local, {@code false} otherwise
222          */
223         public boolean isLocalSone(Sone sone) {
224                 synchronized (localSones) {
225                         return localSones.containsKey(sone.getId());
226                 }
227         }
228
229         /**
230          * Returns whether the given ID is the ID of a local Sone.
231          *
232          * @param id
233          *            The Sone ID to check for its locality
234          * @return {@code true} if the given ID is a local Sone, {@code false}
235          *         otherwise
236          */
237         public boolean isLocalSone(String id) {
238                 synchronized (localSones) {
239                         return localSones.containsKey(id);
240                 }
241         }
242
243         /**
244          * Returns all local Sones.
245          *
246          * @return All local Sones
247          */
248         public Set<Sone> getLocalSones() {
249                 synchronized (localSones) {
250                         return new HashSet<Sone>(localSones.values());
251                 }
252         }
253
254         /**
255          * Returns the local Sone with the given ID.
256          *
257          * @param id
258          *            The ID of the Sone to get
259          * @return The Sone with the given ID
260          */
261         public Sone getLocalSone(String id) {
262                 synchronized (localSones) {
263                         Sone sone = localSones.get(id);
264                         if (sone == null) {
265                                 sone = new Sone(id);
266                                 localSones.put(id, sone);
267                         }
268                         return sone;
269                 }
270         }
271
272         /**
273          * Returns all remote Sones.
274          *
275          * @return All remote Sones
276          */
277         public Set<Sone> getRemoteSones() {
278                 synchronized (remoteSones) {
279                         return new HashSet<Sone>(remoteSones.values());
280                 }
281         }
282
283         /**
284          * Returns the remote Sone with the given ID.
285          *
286          * @param id
287          *            The ID of the remote Sone to get
288          * @return The Sone with the given ID
289          */
290         public Sone getRemoteSone(String id) {
291                 synchronized (remoteSones) {
292                         Sone sone = remoteSones.get(id);
293                         if (sone == null) {
294                                 sone = new Sone(id);
295                                 remoteSones.put(id, sone);
296                         }
297                         return sone;
298                 }
299         }
300
301         /**
302          * Returns whether the given Sone is a remote Sone.
303          *
304          * @param sone
305          *            The Sone to check
306          * @return {@code true} if the given Sone is a remote Sone, {@code false}
307          *         otherwise
308          */
309         public boolean isRemoteSone(Sone sone) {
310                 synchronized (remoteSones) {
311                         return remoteSones.containsKey(sone.getId());
312                 }
313         }
314
315         /**
316          * Returns whether the Sone with the given ID is a remote Sone.
317          *
318          * @param id
319          *            The ID of the Sone to check
320          * @return {@code true} if the Sone with the given ID is a remote Sone,
321          *         {@code false} otherwise
322          */
323         public boolean isRemoteSone(String id) {
324                 synchronized (remoteSones) {
325                         return remoteSones.containsKey(id);
326                 }
327         }
328
329         /**
330          * Returns whether the given Sone is a new Sone. After this check, the Sone
331          * is marked as known, i.e. a second call with the same parameters will
332          * always yield {@code false}.
333          *
334          * @param sone
335          *            The sone to check for
336          * @return {@code true} if the given Sone is new, false otherwise
337          */
338         public boolean isNewSone(Sone sone) {
339                 synchronized (newSones) {
340                         boolean isNew = !knownSones.contains(sone) && newSones.remove(sone);
341                         knownSones.add(sone);
342                         return isNew;
343                 }
344         }
345
346         /**
347          * Returns the post with the given ID.
348          *
349          * @param postId
350          *            The ID of the post to get
351          * @return The post, or {@code null} if there is no such post
352          */
353         public Post getPost(String postId) {
354                 synchronized (posts) {
355                         Post post = posts.get(postId);
356                         if (post == null) {
357                                 post = new Post(postId);
358                                 posts.put(postId, post);
359                         }
360                         return post;
361                 }
362         }
363
364         /**
365          * Returns the reply with the given ID.
366          *
367          * @param replyId
368          *            The ID of the reply to get
369          * @return The reply, or {@code null} if there is no such reply
370          */
371         public Reply getReply(String replyId) {
372                 synchronized (replies) {
373                         Reply reply = replies.get(replyId);
374                         if (reply == null) {
375                                 reply = new Reply(replyId);
376                                 replies.put(replyId, reply);
377                         }
378                         return reply;
379                 }
380         }
381
382         /**
383          * Returns all replies for the given post, order ascending by time.
384          *
385          * @param post
386          *            The post to get all replies for
387          * @return All replies for the given post
388          */
389         public List<Reply> getReplies(Post post) {
390                 Set<Sone> sones = getSones();
391                 List<Reply> replies = new ArrayList<Reply>();
392                 for (Sone sone : sones) {
393                         for (Reply reply : sone.getReplies()) {
394                                 if (reply.getPost().equals(post)) {
395                                         replies.add(reply);
396                                 }
397                         }
398                 }
399                 Collections.sort(replies, Reply.TIME_COMPARATOR);
400                 return replies;
401         }
402
403         /**
404          * Returns all Sones that have liked the given post.
405          *
406          * @param post
407          *            The post to get the liking Sones for
408          * @return The Sones that like the given post
409          */
410         public Set<Sone> getLikes(Post post) {
411                 Set<Sone> sones = new HashSet<Sone>();
412                 for (Sone sone : getSones()) {
413                         if (sone.getLikedPostIds().contains(post.getId())) {
414                                 sones.add(sone);
415                         }
416                 }
417                 return sones;
418         }
419
420         /**
421          * Returns all Sones that have liked the given reply.
422          *
423          * @param reply
424          *            The reply to get the liking Sones for
425          * @return The Sones that like the given reply
426          */
427         public Set<Sone> getLikes(Reply reply) {
428                 Set<Sone> sones = new HashSet<Sone>();
429                 for (Sone sone : getSones()) {
430                         if (sone.getLikedReplyIds().contains(reply.getId())) {
431                                 sones.add(sone);
432                         }
433                 }
434                 return sones;
435         }
436
437         //
438         // ACTIONS
439         //
440
441         /**
442          * Adds a local Sone from the given ID which has to be the ID of an own
443          * identity.
444          *
445          * @param id
446          *            The ID of an own identity to add a Sone for
447          * @return The added (or already existing) Sone
448          */
449         public Sone addLocalSone(String id) {
450                 synchronized (localSones) {
451                         if (localSones.containsKey(id)) {
452                                 logger.log(Level.FINE, "Tried to add known local Sone: %s", id);
453                                 return localSones.get(id);
454                         }
455                         OwnIdentity ownIdentity = identityManager.getOwnIdentity(id);
456                         if (ownIdentity == null) {
457                                 logger.log(Level.INFO, "Invalid Sone ID: %s", id);
458                                 return null;
459                         }
460                         return addLocalSone(ownIdentity);
461                 }
462         }
463
464         /**
465          * Adds a local Sone from the given own identity.
466          *
467          * @param ownIdentity
468          *            The own identity to create a Sone from
469          * @return The added (or already existing) Sone
470          */
471         public Sone addLocalSone(OwnIdentity ownIdentity) {
472                 if (ownIdentity == null) {
473                         logger.log(Level.WARNING, "Given OwnIdentity is null!");
474                         return null;
475                 }
476                 synchronized (localSones) {
477                         final Sone sone;
478                         try {
479                                 sone = getLocalSone(ownIdentity.getId()).setIdentity(ownIdentity).setInsertUri(new FreenetURI(ownIdentity.getInsertUri())).setRequestUri(new FreenetURI(ownIdentity.getRequestUri()));
480                                 sone.setLatestEdition(Numbers.safeParseLong(ownIdentity.getProperty("Sone.LatestEdition"), (long) 0));
481                         } catch (MalformedURLException mue1) {
482                                 logger.log(Level.SEVERE, "Could not convert the Identity’s URIs to Freenet URIs: " + ownIdentity.getInsertUri() + ", " + ownIdentity.getRequestUri(), mue1);
483                                 return null;
484                         }
485                         /* TODO - load posts ’n stuff */
486                         localSones.put(ownIdentity.getId(), sone);
487                         SoneInserter soneInserter = new SoneInserter(this, freenetInterface, sone);
488                         soneInserters.put(sone, soneInserter);
489                         soneInserter.start();
490                         setSoneStatus(sone, SoneStatus.idle);
491                         loadSone(sone);
492                         new Thread(new Runnable() {
493
494                                 @Override
495                                 @SuppressWarnings("synthetic-access")
496                                 public void run() {
497                                         soneDownloader.fetchSone(sone);
498                                 }
499
500                         }, "Sone Downloader").start();
501                         return sone;
502                 }
503         }
504
505         /**
506          * Creates a new Sone for the given own identity.
507          *
508          * @param ownIdentity
509          *            The own identity to create a Sone for
510          * @return The created Sone
511          */
512         public Sone createSone(OwnIdentity ownIdentity) {
513                 identityManager.addContext(ownIdentity, "Sone");
514                 Sone sone = addLocalSone(ownIdentity);
515                 synchronized (sone) {
516                         /* mark as modified so that it gets inserted immediately. */
517                         sone.setModificationCounter(sone.getModificationCounter() + 1);
518                 }
519                 return sone;
520         }
521
522         /**
523          * Adds the Sone of the given identity.
524          *
525          * @param identity
526          *            The identity whose Sone to add
527          * @return The added or already existing Sone
528          */
529         public Sone addRemoteSone(Identity identity) {
530                 if (identity == null) {
531                         logger.log(Level.WARNING, "Given Identity is null!");
532                         return null;
533                 }
534                 synchronized (remoteSones) {
535                         final Sone sone = getRemoteSone(identity.getId()).setIdentity(identity);
536                         boolean newSone = sone.getRequestUri() == null;
537                         sone.setRequestUri(getSoneUri(identity.getRequestUri()));
538                         sone.setLatestEdition(Numbers.safeParseLong(identity.getProperty("Sone.LatestEdition"), (long) 0));
539                         if (newSone) {
540                                 synchronized (newSones) {
541                                         newSones.add(sone);
542                                 }
543                         }
544                         remoteSones.put(identity.getId(), sone);
545                         soneDownloader.addSone(sone);
546                         setSoneStatus(sone, SoneStatus.unknown);
547                         new Thread(new Runnable() {
548
549                                 @Override
550                                 @SuppressWarnings("synthetic-access")
551                                 public void run() {
552                                         soneDownloader.fetchSone(sone);
553                                 }
554
555                         }, "Sone Downloader").start();
556                         return sone;
557                 }
558         }
559
560         /**
561          * Updates the stores Sone with the given Sone.
562          *
563          * @param sone
564          *            The updated Sone
565          */
566         public void updateSone(Sone sone) {
567                 if (isRemoteSone(sone)) {
568                         Sone storedSone = getRemoteSone(sone.getId());
569                         if (!(sone.getTime() > storedSone.getTime())) {
570                                 logger.log(Level.FINE, "Downloaded Sone %s is not newer than stored Sone %s.", new Object[] { sone, storedSone });
571                                 return;
572                         }
573                         synchronized (posts) {
574                                 for (Post post : storedSone.getPosts()) {
575                                         posts.remove(post.getId());
576                                 }
577                                 for (Post post : sone.getPosts()) {
578                                         posts.put(post.getId(), post);
579                                 }
580                         }
581                         synchronized (replies) {
582                                 for (Reply reply : storedSone.getReplies()) {
583                                         replies.remove(reply.getId());
584                                 }
585                                 for (Reply reply : sone.getReplies()) {
586                                         replies.put(reply.getId(), reply);
587                                 }
588                         }
589                         synchronized (storedSone) {
590                                 storedSone.setTime(sone.getTime());
591                                 storedSone.setProfile(sone.getProfile());
592                                 storedSone.setPosts(sone.getPosts());
593                                 storedSone.setReplies(sone.getReplies());
594                                 storedSone.setLikePostIds(sone.getLikedPostIds());
595                                 storedSone.setLikeReplyIds(sone.getLikedReplyIds());
596                                 storedSone.setLatestEdition(sone.getRequestUri().getEdition());
597                                 storedSone.setModificationCounter(0);
598                         }
599                 }
600         }
601
602         /**
603          * Deletes the given Sone. This will remove the Sone from the
604          * {@link #getLocalSone(String) local Sones}, stops its {@link SoneInserter}
605          * and remove the context from its identity.
606          *
607          * @param sone
608          *            The Sone to delete
609          */
610         public void deleteSone(Sone sone) {
611                 if (!(sone.getIdentity() instanceof OwnIdentity)) {
612                         logger.log(Level.WARNING, "Tried to delete Sone of non-own identity: %s", sone);
613                         return;
614                 }
615                 synchronized (localSones) {
616                         if (!localSones.containsKey(sone.getId())) {
617                                 logger.log(Level.WARNING, "Tried to delete non-local Sone: %s", sone);
618                                 return;
619                         }
620                         localSones.remove(sone.getId());
621                         soneInserters.remove(sone).stop();
622                 }
623                 identityManager.removeContext((OwnIdentity) sone.getIdentity(), "Sone");
624                 identityManager.removeProperty((OwnIdentity) sone.getIdentity(), "Sone.LatestEdition");
625                 try {
626                         configuration.getLongValue("Sone/" + sone.getId() + "/Time").setValue(null);
627                 } catch (ConfigurationException ce1) {
628                         logger.log(Level.WARNING, "Could not remove Sone from configuration!", ce1);
629                 }
630         }
631
632         /**
633          * Loads and updates the given Sone from the configuration. If any error is
634          * encountered, loading is aborted and the given Sone is not changed.
635          *
636          * @param sone
637          *            The Sone to load and update
638          */
639         public void loadSone(Sone sone) {
640                 if (!isLocalSone(sone)) {
641                         logger.log(Level.FINE, "Tried to load non-local Sone: %s", sone);
642                         return;
643                 }
644
645                 /* load Sone. */
646                 String sonePrefix = "Sone/" + sone.getId();
647                 Long soneTime = configuration.getLongValue(sonePrefix + "/Time").getValue(null);
648                 if (soneTime == null) {
649                         logger.log(Level.INFO, "Could not load Sone because no Sone has been saved.");
650                         return;
651                 }
652                 long soneModificationCounter = configuration.getLongValue(sonePrefix + "/ModificationCounter").getValue((long) 0);
653
654                 /* load profile. */
655                 Profile profile = new Profile();
656                 profile.setFirstName(configuration.getStringValue(sonePrefix + "/Profile/FirstName").getValue(null));
657                 profile.setMiddleName(configuration.getStringValue(sonePrefix + "/Profile/MiddleName").getValue(null));
658                 profile.setLastName(configuration.getStringValue(sonePrefix + "/Profile/LastName").getValue(null));
659                 profile.setBirthDay(configuration.getIntValue(sonePrefix + "/Profile/BirthDay").getValue(null));
660                 profile.setBirthMonth(configuration.getIntValue(sonePrefix + "/Profile/BirthMonth").getValue(null));
661                 profile.setBirthYear(configuration.getIntValue(sonePrefix + "/Profile/BirthYear").getValue(null));
662
663                 /* load posts. */
664                 Set<Post> posts = new HashSet<Post>();
665                 while (true) {
666                         String postPrefix = sonePrefix + "/Posts/" + posts.size();
667                         String postId = configuration.getStringValue(postPrefix + "/ID").getValue(null);
668                         if (postId == null) {
669                                 break;
670                         }
671                         long postTime = configuration.getLongValue(postPrefix + "/Time").getValue((long) 0);
672                         String postText = configuration.getStringValue(postPrefix + "/Text").getValue(null);
673                         if ((postTime == 0) || (postText == null)) {
674                                 logger.log(Level.WARNING, "Invalid post found, aborting load!");
675                                 return;
676                         }
677                         posts.add(getPost(postId).setSone(sone).setTime(postTime).setText(postText));
678                 }
679
680                 /* load replies. */
681                 Set<Reply> replies = new HashSet<Reply>();
682                 while (true) {
683                         String replyPrefix = sonePrefix + "/Replies/" + replies.size();
684                         String replyId = configuration.getStringValue(replyPrefix + "/ID").getValue(null);
685                         if (replyId == null) {
686                                 break;
687                         }
688                         String postId = configuration.getStringValue(replyPrefix + "/Post/ID").getValue(null);
689                         long replyTime = configuration.getLongValue(replyPrefix + "/Time").getValue((long) 0);
690                         String replyText = configuration.getStringValue(replyPrefix + "/Text").getValue(null);
691                         if ((postId == null) || (replyTime == 0) || (replyText == null)) {
692                                 logger.log(Level.WARNING, "Invalid reply found, aborting load!");
693                                 return;
694                         }
695                         replies.add(getReply(replyId).setSone(sone).setPost(getPost(postId)).setTime(replyTime).setText(replyText));
696                 }
697
698                 /* load post likes. */
699                 Set<String> likedPostIds = new HashSet<String>();
700                 while (true) {
701                         String likedPostId = configuration.getStringValue(sonePrefix + "/Likes/Post/" + likedPostIds.size() + "/ID").getValue(null);
702                         if (likedPostId == null) {
703                                 break;
704                         }
705                         likedPostIds.add(likedPostId);
706                 }
707
708                 /* load reply likes. */
709                 Set<String> likedReplyIds = new HashSet<String>();
710                 while (true) {
711                         String likedReplyId = configuration.getStringValue(sonePrefix + "/Likes/Reply/" + likedReplyIds.size() + "/ID").getValue(null);
712                         if (likedReplyId == null) {
713                                 break;
714                         }
715                         likedReplyIds.add(likedReplyId);
716                 }
717
718                 /* load friends. */
719                 Set<Sone> friends = new HashSet<Sone>();
720                 while (true) {
721                         String friendId = configuration.getStringValue(sonePrefix + "/Friends/" + friends.size() + "/ID").getValue(null);
722                         if (friendId == null) {
723                                 break;
724                         }
725                         Boolean friendLocal = configuration.getBooleanValue(sonePrefix + "/Friends/" + friends.size() + "/Local").getValue(null);
726                         if (friendLocal == null) {
727                                 logger.log(Level.WARNING, "Invalid friend found, aborting load!");
728                                 return;
729                         }
730                         friends.add(friendLocal ? getLocalSone(friendId) : getRemoteSone(friendId));
731                 }
732
733                 /* if we’re still here, Sone was loaded successfully. */
734                 synchronized (sone) {
735                         sone.setTime(soneTime);
736                         sone.setProfile(profile);
737                         sone.setPosts(posts);
738                         sone.setReplies(replies);
739                         sone.setLikePostIds(likedPostIds);
740                         sone.setLikeReplyIds(likedReplyIds);
741                         sone.setFriends(friends);
742                         sone.setModificationCounter(soneModificationCounter);
743                 }
744                 synchronized (newSones) {
745                         for (Sone friend : friends) {
746                                 knownSones.add(friend);
747                         }
748                 }
749         }
750
751         /**
752          * Saves the given Sone. This will persist all local settings for the given
753          * Sone, such as the friends list and similar, private options.
754          *
755          * @param sone
756          *            The Sone to save
757          */
758         public void saveSone(Sone sone) {
759                 if (!isLocalSone(sone)) {
760                         logger.log(Level.FINE, "Tried to save non-local Sone: %s", sone);
761                         return;
762                 }
763                 if (!(sone.getIdentity() instanceof OwnIdentity)) {
764                         logger.log(Level.WARNING, "Local Sone without OwnIdentity found, refusing to save: %s", sone);
765                         return;
766                 }
767
768                 logger.log(Level.INFO, "Saving Sone: %s", sone);
769                 identityManager.setProperty((OwnIdentity) sone.getIdentity(), "Sone.LatestEdition", String.valueOf(sone.getLatestEdition()));
770                 try {
771                         /* save Sone into configuration. */
772                         String sonePrefix = "Sone/" + sone.getId();
773                         configuration.getLongValue(sonePrefix + "/Time").setValue(sone.getTime());
774                         configuration.getLongValue(sonePrefix + "/ModificationCounter").setValue(sone.getModificationCounter());
775
776                         /* save profile. */
777                         Profile profile = sone.getProfile();
778                         configuration.getStringValue(sonePrefix + "/Profile/FirstName").setValue(profile.getFirstName());
779                         configuration.getStringValue(sonePrefix + "/Profile/MiddleName").setValue(profile.getMiddleName());
780                         configuration.getStringValue(sonePrefix + "/Profile/LastName").setValue(profile.getLastName());
781                         configuration.getIntValue(sonePrefix + "/Profile/BirthDay").setValue(profile.getBirthDay());
782                         configuration.getIntValue(sonePrefix + "/Profile/BirthMonth").setValue(profile.getBirthMonth());
783                         configuration.getIntValue(sonePrefix + "/Profile/BirthYear").setValue(profile.getBirthYear());
784
785                         /* save posts. */
786                         int postCounter = 0;
787                         for (Post post : sone.getPosts()) {
788                                 String postPrefix = sonePrefix + "/Posts/" + postCounter++;
789                                 configuration.getStringValue(postPrefix + "/ID").setValue(post.getId());
790                                 configuration.getLongValue(postPrefix + "/Time").setValue(post.getTime());
791                                 configuration.getStringValue(postPrefix + "/Text").setValue(post.getText());
792                         }
793                         configuration.getStringValue(sonePrefix + "/Posts/" + postCounter + "/ID").setValue(null);
794
795                         /* save replies. */
796                         int replyCounter = 0;
797                         for (Reply reply : sone.getReplies()) {
798                                 String replyPrefix = sonePrefix + "/Replies/" + replyCounter++;
799                                 configuration.getStringValue(replyPrefix + "/ID").setValue(reply.getId());
800                                 configuration.getStringValue(replyPrefix + "/Post/ID").setValue(reply.getPost().getId());
801                                 configuration.getLongValue(replyPrefix + "/Time").setValue(reply.getTime());
802                                 configuration.getStringValue(replyPrefix + "/Text").setValue(reply.getText());
803                         }
804                         configuration.getStringValue(sonePrefix + "/Replies/" + replyCounter + "/ID").setValue(null);
805
806                         /* save post likes. */
807                         int postLikeCounter = 0;
808                         for (String postId : sone.getLikedPostIds()) {
809                                 configuration.getStringValue(sonePrefix + "/Likes/Post/" + postLikeCounter++ + "/ID").setValue(postId);
810                         }
811                         configuration.getStringValue(sonePrefix + "/Likes/Post/" + postLikeCounter + "/ID").setValue(null);
812
813                         /* save reply likes. */
814                         int replyLikeCounter = 0;
815                         for (String replyId : sone.getLikedReplyIds()) {
816                                 configuration.getStringValue(sonePrefix + "/Likes/Reply/" + replyLikeCounter++ + "/ID").setValue(replyId);
817                         }
818                         configuration.getStringValue(sonePrefix + "/Likes/Reply/" + replyLikeCounter + "/ID").setValue(null);
819
820                         /* save friends. */
821                         int friendCounter = 0;
822                         for (Sone friend : sone.getFriends()) {
823                                 configuration.getStringValue(sonePrefix + "/Friends/" + friendCounter + "/ID").setValue(friend.getId());
824                                 configuration.getBooleanValue(sonePrefix + "/Friends/" + friendCounter++ + "/Local").setValue(friend.getInsertUri() != null);
825                         }
826                         configuration.getStringValue(sonePrefix + "/Friends/" + friendCounter + "/ID").setValue(null);
827
828                         logger.log(Level.INFO, "Sone %s saved.", sone);
829                 } catch (ConfigurationException ce1) {
830                         logger.log(Level.WARNING, "Could not save Sone: " + sone, ce1);
831                 }
832         }
833
834         /**
835          * Creates a new post.
836          *
837          * @param sone
838          *            The Sone that creates the post
839          * @param text
840          *            The text of the post
841          */
842         public void createPost(Sone sone, String text) {
843                 createPost(sone, System.currentTimeMillis(), text);
844         }
845
846         /**
847          * Creates a new post.
848          *
849          * @param sone
850          *            The Sone that creates the post
851          * @param time
852          *            The time of the post
853          * @param text
854          *            The text of the post
855          */
856         public void createPost(Sone sone, long time, String text) {
857                 if (!isLocalSone(sone)) {
858                         logger.log(Level.FINE, "Tried to create post for non-local Sone: %s", sone);
859                         return;
860                 }
861                 Post post = new Post(sone, time, text);
862                 synchronized (posts) {
863                         posts.put(post.getId(), post);
864                 }
865                 sone.addPost(post);
866                 saveSone(sone);
867         }
868
869         /**
870          * Deletes the given post.
871          *
872          * @param post
873          *            The post to delete
874          */
875         public void deletePost(Post post) {
876                 if (!isLocalSone(post.getSone())) {
877                         logger.log(Level.WARNING, "Tried to delete post of non-local Sone: %s", post.getSone());
878                         return;
879                 }
880                 post.getSone().removePost(post);
881                 synchronized (posts) {
882                         posts.remove(post.getId());
883                 }
884                 saveSone(post.getSone());
885         }
886
887         /**
888          * Creates a new reply.
889          *
890          * @param sone
891          *            The Sone that creates the reply
892          * @param post
893          *            The post that this reply refers to
894          * @param text
895          *            The text of the reply
896          */
897         public void createReply(Sone sone, Post post, String text) {
898                 createReply(sone, post, System.currentTimeMillis(), text);
899         }
900
901         /**
902          * Creates a new reply.
903          *
904          * @param sone
905          *            The Sone that creates the reply
906          * @param post
907          *            The post that this reply refers to
908          * @param time
909          *            The time of the reply
910          * @param text
911          *            The text of the reply
912          */
913         public void createReply(Sone sone, Post post, long time, String text) {
914                 if (!isLocalSone(sone)) {
915                         logger.log(Level.FINE, "Tried to create reply for non-local Sone: %s", sone);
916                         return;
917                 }
918                 Reply reply = new Reply(sone, post, System.currentTimeMillis(), text);
919                 synchronized (replies) {
920                         replies.put(reply.getId(), reply);
921                 }
922                 sone.addReply(reply);
923                 saveSone(sone);
924         }
925
926         /**
927          * Deletes the given reply.
928          *
929          * @param reply
930          *            The reply to delete
931          */
932         public void deleteReply(Reply reply) {
933                 Sone sone = reply.getSone();
934                 if (!isLocalSone(sone)) {
935                         logger.log(Level.FINE, "Tried to delete non-local reply: %s", reply);
936                         return;
937                 }
938                 synchronized (replies) {
939                         replies.remove(reply.getId());
940                 }
941                 sone.removeReply(reply);
942                 saveSone(sone);
943         }
944
945         /**
946          * Starts the core.
947          */
948         public void start() {
949                 loadConfiguration();
950         }
951
952         /**
953          * Stops the core.
954          */
955         public void stop() {
956                 synchronized (localSones) {
957                         for (SoneInserter soneInserter : soneInserters.values()) {
958                                 soneInserter.stop();
959                         }
960                 }
961                 saveConfiguration();
962         }
963
964         //
965         // PRIVATE METHODS
966         //
967
968         /**
969          * Loads the configuration.
970          */
971         @SuppressWarnings("unchecked")
972         private void loadConfiguration() {
973                 /* create options. */
974                 options.addIntegerOption("InsertionDelay", new DefaultOption<Integer>(60, new OptionWatcher<Integer>() {
975
976                         @Override
977                         public void optionChanged(Option<Integer> option, Integer oldValue, Integer newValue) {
978                                 SoneInserter.setInsertionDelay(newValue);
979                         }
980
981                 }));
982                 options.addBooleanOption("ClearOnNextRestart", new DefaultOption<Boolean>(false));
983                 options.addBooleanOption("ReallyClearOnNextRestart", new DefaultOption<Boolean>(false));
984
985                 /* read options from configuration. */
986                 options.getBooleanOption("ClearOnNextRestart").set(configuration.getBooleanValue("Option/ClearOnNextRestart").getValue(null));
987                 options.getBooleanOption("ReallyClearOnNextRestart").set(configuration.getBooleanValue("Option/ReallyClearOnNextRestart").getValue(null));
988                 boolean clearConfiguration = options.getBooleanOption("ClearOnNextRestart").get() && options.getBooleanOption("ReallyClearOnNextRestart").get();
989                 options.getBooleanOption("ClearOnNextRestart").set(null);
990                 options.getBooleanOption("ReallyClearOnNextRestart").set(null);
991                 if (clearConfiguration) {
992                         /* stop loading the configuration. */
993                         return;
994                 }
995
996                 options.getIntegerOption("InsertionDelay").set(configuration.getIntValue("Option/InsertionDelay").getValue(null));
997
998                 /* load known Sones. */
999                 int soneCounter = 0;
1000                 while (true) {
1001                         String knownSoneId = configuration.getStringValue("KnownSone/" + soneCounter++ + "/ID").getValue(null);
1002                         if (knownSoneId == null) {
1003                                 break;
1004                         }
1005                         synchronized (newSones) {
1006                                 knownSones.add(getRemoteSone(knownSoneId));
1007                         }
1008                 }
1009         }
1010
1011         /**
1012          * Saves the current options.
1013          */
1014         private void saveConfiguration() {
1015                 /* store the options first. */
1016                 try {
1017                         configuration.getIntValue("Option/InsertionDelay").setValue(options.getIntegerOption("InsertionDelay").getReal());
1018                         configuration.getBooleanValue("Option/ClearOnNextRestart").setValue(options.getBooleanOption("ClearOnNextRestart").getReal());
1019                         configuration.getBooleanValue("Option/ReallyClearOnNextRestart").setValue(options.getBooleanOption("ReallyClearOnNextRestart").getReal());
1020
1021                         /* save known Sones. */
1022                         int soneCounter = 0;
1023                         synchronized (newSones) {
1024                                 for (Sone knownSone : knownSones) {
1025                                         configuration.getStringValue("KnownSone/" + soneCounter++ + "/ID").setValue(knownSone.getId());
1026                                 }
1027                                 configuration.getStringValue("KnownSone/" + soneCounter + "/ID").setValue(null);
1028                         }
1029
1030                 } catch (ConfigurationException ce1) {
1031                         logger.log(Level.SEVERE, "Could not store configuration!", ce1);
1032                 }
1033         }
1034
1035         /**
1036          * Generate a Sone URI from the given URI and latest edition.
1037          *
1038          * @param uriString
1039          *            The URI to derive the Sone URI from
1040          * @return The derived URI
1041          */
1042         private FreenetURI getSoneUri(String uriString) {
1043                 try {
1044                         FreenetURI uri = new FreenetURI(uriString).setDocName("Sone").setMetaString(new String[0]);
1045                         return uri;
1046                 } catch (MalformedURLException mue1) {
1047                         logger.log(Level.WARNING, "Could not create Sone URI from URI: " + uriString, mue1);
1048                         return null;
1049                 }
1050         }
1051
1052         //
1053         // INTERFACE IdentityListener
1054         //
1055
1056         /**
1057          * {@inheritDoc}
1058          */
1059         @Override
1060         public void ownIdentityAdded(OwnIdentity ownIdentity) {
1061                 logger.log(Level.FINEST, "Adding OwnIdentity: " + ownIdentity);
1062                 if (ownIdentity.hasContext("Sone")) {
1063                         addLocalSone(ownIdentity);
1064                 }
1065         }
1066
1067         /**
1068          * {@inheritDoc}
1069          */
1070         @Override
1071         public void ownIdentityRemoved(OwnIdentity ownIdentity) {
1072                 logger.log(Level.FINEST, "Removing OwnIdentity: " + ownIdentity);
1073         }
1074
1075         /**
1076          * {@inheritDoc}
1077          */
1078         @Override
1079         public void identityAdded(Identity identity) {
1080                 logger.log(Level.FINEST, "Adding Identity: " + identity);
1081                 addRemoteSone(identity);
1082         }
1083
1084         /**
1085          * {@inheritDoc}
1086          */
1087         @Override
1088         public void identityUpdated(final Identity identity) {
1089                 new Thread(new Runnable() {
1090
1091                         @Override
1092                         @SuppressWarnings("synthetic-access")
1093                         public void run() {
1094                                 Sone sone = getRemoteSone(identity.getId());
1095                                 soneDownloader.fetchSone(sone);
1096                         }
1097                 }).start();
1098         }
1099
1100         /**
1101          * {@inheritDoc}
1102          */
1103         @Override
1104         public void identityRemoved(Identity identity) {
1105                 /* TODO */
1106         }
1107
1108 }