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