Add default Sones after resetting the configuration.
[Sone.git] / src / main / java / net / pterodactylus / sone / core / Core.java
1 /*
2  * FreenetSone - 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.io.InputStream;
21 import java.net.MalformedURLException;
22 import java.util.ArrayList;
23 import java.util.Collection;
24 import java.util.Collections;
25 import java.util.Comparator;
26 import java.util.HashMap;
27 import java.util.HashSet;
28 import java.util.List;
29 import java.util.Map;
30 import java.util.Set;
31 import java.util.UUID;
32 import java.util.logging.Level;
33 import java.util.logging.Logger;
34
35 import net.pterodactylus.sone.core.Options.DefaultOption;
36 import net.pterodactylus.sone.core.Options.Option;
37 import net.pterodactylus.sone.core.Options.OptionWatcher;
38 import net.pterodactylus.sone.core.SoneException.Type;
39 import net.pterodactylus.sone.data.Post;
40 import net.pterodactylus.sone.data.Profile;
41 import net.pterodactylus.sone.data.Reply;
42 import net.pterodactylus.sone.data.Sone;
43 import net.pterodactylus.util.config.Configuration;
44 import net.pterodactylus.util.config.ConfigurationException;
45 import net.pterodactylus.util.filter.Filter;
46 import net.pterodactylus.util.filter.Filters;
47 import net.pterodactylus.util.logging.Logging;
48 import net.pterodactylus.util.service.AbstractService;
49 import freenet.client.FetchResult;
50 import freenet.keys.FreenetURI;
51
52 /**
53  * The Sone core.
54  *
55  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
56  */
57 public class Core extends AbstractService {
58
59         /**
60          * Enumeration for the possible states of a {@link Sone}.
61          *
62          * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
63          */
64         public enum SoneStatus {
65
66                 /** The Sone is unknown, i.e. not yet downloaded. */
67                 unknown,
68
69                 /** The Sone is idle, i.e. not being downloaded or inserted. */
70                 idle,
71
72                 /** The Sone is currently being inserted. */
73                 inserting,
74
75                 /** The Sone is currently being downloaded. */
76                 downloading,
77         }
78
79         /** The logger. */
80         private static final Logger logger = Logging.getLogger(Core.class);
81
82         /** The options. */
83         private final Options options = new Options();
84
85         /** The configuration. */
86         private Configuration configuration;
87
88         /** Interface to freenet. */
89         private FreenetInterface freenetInterface;
90
91         /** The Sone downloader. */
92         private SoneDownloader soneDownloader;
93
94         /** The Sone blacklist. */
95         private final Set<Sone> blacklistedSones = new HashSet<Sone>();
96
97         /** The local Sones. */
98         private final Set<Sone> localSones = new HashSet<Sone>();
99
100         /** Sone inserters. */
101         private final Map<Sone, SoneInserter> soneInserters = new HashMap<Sone, SoneInserter>();
102
103         /** The Sones’ statuses. */
104         private final Map<Sone, SoneStatus> soneStatuses = Collections.synchronizedMap(new HashMap<Sone, SoneStatus>());
105
106         /* various caches follow here. */
107
108         /** Cache for all known Sones. */
109         private final Map<String, Sone> soneCache = new HashMap<String, Sone>();
110
111         /** Cache for all known posts. */
112         private final Map<String, Post> postCache = new HashMap<String, Post>();
113
114         /** Cache for all known replies. */
115         private final Map<String, Reply> replyCache = new HashMap<String, Reply>();
116
117         /**
118          * Creates a new core.
119          */
120         public Core() {
121                 super("Sone Core", false);
122         }
123
124         //
125         // ACCESSORS
126         //
127
128         /**
129          * Returns the options of the Sone plugin.
130          *
131          * @return The options of the Sone plugin
132          */
133         public Options getOptions() {
134                 return options;
135         }
136
137         /**
138          * Sets the configuration of the core.
139          *
140          * @param configuration
141          *            The configuration of the core
142          * @return This core (for method chaining)
143          */
144         public Core configuration(Configuration configuration) {
145                 this.configuration = configuration;
146                 return this;
147         }
148
149         /**
150          * Sets the Freenet interface to use.
151          *
152          * @param freenetInterface
153          *            The Freenet interface to use
154          * @return This core (for method chaining)
155          */
156         public Core freenetInterface(FreenetInterface freenetInterface) {
157                 this.freenetInterface = freenetInterface;
158                 soneDownloader = new SoneDownloader(this, freenetInterface);
159                 soneDownloader.start();
160                 return this;
161         }
162
163         /**
164          * Returns the local Sones.
165          *
166          * @return The local Sones
167          */
168         public Set<Sone> getSones() {
169                 return Filters.filteredSet(localSones, new Filter<Sone>() {
170
171                         /**
172                          * {@inheritDoc}
173                          */
174                         @Override
175                         @SuppressWarnings("synthetic-access")
176                         public boolean filterObject(Sone sone) {
177                                 return !blacklistedSones.contains(sone);
178                         }
179                 });
180         }
181
182         /**
183          * Returns the Sone with the given ID, or an empty Sone that has been
184          * initialized with the given ID.
185          *
186          * @param soneId
187          *            The ID of the Sone
188          * @return The Sone
189          */
190         public Sone getSone(String soneId) {
191                 if (!soneCache.containsKey(soneId)) {
192                         Sone sone = new Sone(soneId);
193                         soneCache.put(soneId, sone);
194                         setSoneStatus(sone, SoneStatus.unknown);
195                 }
196                 return soneCache.get(soneId);
197         }
198
199         /**
200          * Returns all known sones.
201          *
202          * @return All known sones
203          */
204         public Collection<Sone> getKnownSones() {
205                 return Filters.filteredCollection(soneCache.values(), new Filter<Sone>() {
206
207                         /**
208                          * {@inheritDoc}
209                          */
210                         @Override
211                         @SuppressWarnings("synthetic-access")
212                         public boolean filterObject(Sone sone) {
213                                 return !blacklistedSones.contains(sone);
214                         }
215                 });
216         }
217
218         /**
219          * Gets all known Sones that are not local Sones.
220          *
221          * @return All remote Sones
222          */
223         public Collection<Sone> getRemoteSones() {
224                 return Filters.filteredCollection(getKnownSones(), new Filter<Sone>() {
225
226                         @Override
227                         @SuppressWarnings("synthetic-access")
228                         public boolean filterObject(Sone sone) {
229                                 return !blacklistedSones.contains(sone) && !localSones.contains(sone);
230                         }
231                 });
232         }
233
234         /**
235          * Returns all blacklisted Sones.
236          *
237          * @return All blacklisted Sones
238          */
239         public Collection<Sone> getBlacklistedSones() {
240                 return Collections.unmodifiableCollection(blacklistedSones);
241         }
242
243         /**
244          * Checks whether the given Sone is blacklisted.
245          *
246          * @param sone
247          *            The Sone to check
248          * @return {@code true} if this Sone is blacklisted, {@code false} otherwise
249          */
250         public boolean isBlacklistedSone(Sone sone) {
251                 return blacklistedSones.contains(sone);
252         }
253
254         /**
255          * Returns the status of the given Sone.
256          *
257          * @param sone
258          *            The Sone to get the status for
259          * @return The status of the Sone
260          */
261         public SoneStatus getSoneStatus(Sone sone) {
262                 return soneStatuses.get(sone);
263         }
264
265         /**
266          * Sets the status of the Sone.
267          *
268          * @param sone
269          *            The Sone to set the status for
270          * @param soneStatus
271          *            The status of the Sone
272          */
273         public void setSoneStatus(Sone sone, SoneStatus soneStatus) {
274                 soneStatuses.put(sone, soneStatus);
275         }
276
277         /**
278          * Creates a new post and adds it to the given Sone.
279          *
280          * @param sone
281          *            The sone that creates the post
282          * @param text
283          *            The text of the post
284          * @return The created post
285          */
286         public Post createPost(Sone sone, String text) {
287                 return createPost(sone, System.currentTimeMillis(), text);
288         }
289
290         /**
291          * Creates a new post and adds it to the given Sone.
292          *
293          * @param sone
294          *            The Sone that creates the post
295          * @param time
296          *            The time of the post
297          * @param text
298          *            The text of the post
299          * @return The created post
300          */
301         public Post createPost(Sone sone, long time, String text) {
302                 Post post = getPost(UUID.randomUUID().toString()).setSone(sone).setTime(time).setText(text);
303                 sone.addPost(post);
304                 return post;
305         }
306
307         /**
308          * Creates a reply.
309          *
310          * @param sone
311          *            The Sone that posts the reply
312          * @param post
313          *            The post the reply refers to
314          * @param text
315          *            The text of the reply
316          * @return The created reply
317          */
318         public Reply createReply(Sone sone, Post post, String text) {
319                 return createReply(sone, post, System.currentTimeMillis(), text);
320         }
321
322         /**
323          * Creates a reply.
324          *
325          * @param sone
326          *            The Sone that posts the reply
327          * @param post
328          *            The post the reply refers to
329          * @param time
330          *            The time of the post
331          * @param text
332          *            The text of the reply
333          * @return The created reply
334          */
335         public Reply createReply(Sone sone, Post post, long time, String text) {
336                 Reply reply = getReply(UUID.randomUUID().toString()).setSone(sone).setPost(post).setTime(time).setText(text);
337                 sone.addReply(reply);
338                 return reply;
339         }
340
341         //
342         // ACTIONS
343         //
344
345         /**
346          * Adds a Sone to watch for updates. The Sone needs to be completely
347          * initialized.
348          *
349          * @param sone
350          *            The Sone to watch for updates
351          */
352         public void addSone(Sone sone) {
353                 soneCache.put(sone.getId(), sone);
354                 if (!localSones.contains(sone)) {
355                         soneDownloader.addSone(sone);
356                 }
357         }
358
359         /**
360          * Adds the given Sone.
361          *
362          * @param sone
363          *            The Sone to add
364          */
365         public void addLocalSone(Sone sone) {
366                 if (localSones.add(sone)) {
367                         setSoneStatus(sone, SoneStatus.idle);
368                         SoneInserter soneInserter = new SoneInserter(this, freenetInterface, sone);
369                         soneInserter.start();
370                         soneInserters.put(sone, soneInserter);
371                 }
372         }
373
374         /**
375          * Blackslists the given Sone.
376          *
377          * @param sone
378          *            The Sone to blacklist
379          */
380         public void blacklistSone(Sone sone) {
381                 if (blacklistedSones.add(sone)) {
382                         soneDownloader.removeSone(sone);
383                         if (localSones.remove(sone)) {
384                                 SoneInserter soneInserter = soneInserters.remove(sone);
385                                 soneInserter.stop();
386                         }
387                 }
388         }
389
390         /**
391          * Unblacklists the given Sone.
392          *
393          * @param sone
394          *            The Sone to unblacklist
395          */
396         public void unblacklistSone(Sone sone) {
397                 if (blacklistedSones.remove(sone)) {
398                         if (sone.getInsertUri() != null) {
399                                 addLocalSone(sone);
400                         } else {
401                                 addSone(sone);
402                         }
403                 }
404         }
405
406         /**
407          * Creates a new Sone at a random location.
408          *
409          * @param name
410          *            The name of the Sone
411          * @return The created Sone
412          * @throws SoneException
413          *             if a Sone error occurs
414          */
415         public Sone createSone(String name) throws SoneException {
416                 return createSone(name, "Sone", null, null);
417         }
418
419         /**
420          * Creates a new Sone at the given location. If one of {@code requestUri} or
421          * {@code insertUrI} is {@code null}, the Sone is created at a random
422          * location.
423          *
424          * @param name
425          *            The name of the Sone
426          * @param documentName
427          *            The document name in the SSK
428          * @param requestUri
429          *            The request URI of the Sone, or {@link NullPointerException}
430          *            to create a Sone at a random location
431          * @param insertUri
432          *            The insert URI of the Sone, or {@code null} to create a Sone
433          *            at a random location
434          * @return The created Sone
435          * @throws SoneException
436          *             if a Sone error occurs
437          */
438         public Sone createSone(String name, String documentName, String requestUri, String insertUri) throws SoneException {
439                 if ((name == null) || (name.trim().length() == 0)) {
440                         throw new SoneException(Type.INVALID_SONE_NAME);
441                 }
442                 String finalRequestUri;
443                 String finalInsertUri;
444                 if ((requestUri == null) || (insertUri == null)) {
445                         String[] keyPair = freenetInterface.generateKeyPair();
446                         finalRequestUri = keyPair[0];
447                         finalInsertUri = keyPair[1];
448                 } else {
449                         finalRequestUri = requestUri;
450                         finalInsertUri = insertUri;
451                 }
452                 Sone sone;
453                 try {
454                         logger.log(Level.FINEST, "Creating new Sone “%s” at %s (%s)…", new Object[] { name, finalRequestUri, finalInsertUri });
455                         sone = getSone(UUID.randomUUID().toString()).setName(name).setRequestUri(new FreenetURI(finalRequestUri).setKeyType("USK").setDocName(documentName)).setInsertUri(new FreenetURI(finalInsertUri).setKeyType("USK").setDocName(documentName));
456                         sone.setProfile(new Profile());
457                         /* set modification counter to 1 so it is inserted immediately. */
458                         sone.setModificationCounter(1);
459                         addLocalSone(sone);
460                 } catch (MalformedURLException mue1) {
461                         throw new SoneException(Type.INVALID_URI);
462                 }
463                 return sone;
464         }
465
466         /**
467          * Loads the Sone from the given request URI. The fetching of the data is
468          * performed in a new thread so this method returns immediately.
469          *
470          * @param requestUri
471          *            The request URI to load the Sone from
472          */
473         public void loadSone(final String requestUri) {
474                 loadSone(requestUri, null);
475         }
476
477         /**
478          * Loads the Sone from the given request URI. The fetching of the data is
479          * performed in a new thread so this method returns immediately. If
480          * {@code insertUri} is not {@code null} the loaded Sone is converted into a
481          * local Sone and available using as any other local Sone.
482          *
483          * @param requestUri
484          *            The request URI to load the Sone from
485          * @param insertUri
486          *            The insert URI of the Sone
487          */
488         public void loadSone(final String requestUri, final String insertUri) {
489                 new Thread(new Runnable() {
490
491                         @Override
492                         @SuppressWarnings("synthetic-access")
493                         public void run() {
494                                 try {
495                                         FreenetURI realRequestUri = new FreenetURI(requestUri).setMetaString(new String[] { "sone.xml" });
496                                         FetchResult fetchResult = freenetInterface.fetchUri(realRequestUri);
497                                         if (fetchResult == null) {
498                                                 return;
499                                         }
500                                         Sone parsedSone = soneDownloader.parseSone(null, fetchResult, realRequestUri);
501                                         if (parsedSone != null) {
502                                                 if (insertUri != null) {
503                                                         parsedSone.setInsertUri(new FreenetURI(insertUri));
504                                                         addLocalSone(parsedSone);
505                                                 } else {
506                                                         addSone(parsedSone);
507                                                 }
508                                                 setSoneStatus(parsedSone, SoneStatus.idle);
509                                         }
510                                 } catch (MalformedURLException mue1) {
511                                         logger.log(Level.INFO, "Could not create URI from “" + requestUri + "”.", mue1);
512                                 }
513                         }
514                 }, "Sone Downloader").start();
515         }
516
517         /**
518          * Loads a Sone from an input stream.
519          *
520          * @param soneInputStream
521          *            The input stream to load the Sone from
522          * @return The parsed Sone, or {@code null} if the Sone could not be parsed
523          */
524         public Sone loadSone(InputStream soneInputStream) {
525                 Sone parsedSone = soneDownloader.parseSone(soneInputStream);
526                 if (parsedSone == null) {
527                         return null;
528                 }
529                 if (parsedSone.getInsertUri() != null) {
530                         addLocalSone(parsedSone);
531                 } else {
532                         addSone(parsedSone);
533                 }
534                 return parsedSone;
535         }
536
537         /**
538          * Loads and updates the given Sone.
539          *
540          * @param sone
541          *            The Sone to load
542          */
543         public void loadSone(final Sone sone) {
544                 new Thread(new Runnable() {
545
546                         @Override
547                         @SuppressWarnings("synthetic-access")
548                         public void run() {
549                                 FreenetURI realRequestUri = sone.getRequestUri().setMetaString(new String[] { "sone.xml" });
550                                 setSoneStatus(sone, SoneStatus.downloading);
551                                 try {
552                                         FetchResult fetchResult = freenetInterface.fetchUri(realRequestUri);
553                                         if (fetchResult == null) {
554                                                 /* TODO - mark Sone as bad. */
555                                                 return;
556                                         }
557                                         Sone parsedSone = soneDownloader.parseSone(sone, fetchResult, realRequestUri);
558                                         if (parsedSone != null) {
559                                                 addSone(parsedSone);
560                                         }
561                                 } finally {
562                                         setSoneStatus(sone, (sone.getTime() == 0) ? SoneStatus.unknown : SoneStatus.idle);
563                                 }
564                         }
565                 }, "Sone Downloader").start();
566         }
567
568         /**
569          * Deletes the given Sone from this plugin instance.
570          *
571          * @param sone
572          *            The sone to delete
573          */
574         public void deleteSone(Sone sone) {
575                 SoneInserter soneInserter = soneInserters.remove(sone);
576                 soneInserter.stop();
577                 localSones.remove(sone);
578                 soneStatuses.remove(sone);
579                 soneCache.remove(sone.getId());
580         }
581
582         /**
583          * Returns the post with the given ID. If no post exists yet with the given
584          * ID, a new post is returned.
585          *
586          * @param postId
587          *            The ID of the post
588          * @return The post
589          */
590         public Post getPost(String postId) {
591                 if (!postCache.containsKey(postId)) {
592                         postCache.put(postId, new Post(postId));
593                 }
594                 return postCache.get(postId);
595         }
596
597         /**
598          * Returns the reply with the given ID. If no reply exists yet with the
599          * given ID, a new reply is returned.
600          *
601          * @param replyId
602          *            The ID of the reply
603          * @return The reply
604          */
605         public Reply getReply(String replyId) {
606                 if (!replyCache.containsKey(replyId)) {
607                         replyCache.put(replyId, new Reply(replyId));
608                 }
609                 return replyCache.get(replyId);
610         }
611
612         /**
613          * Gets all replies to the given post, sorted by date, oldest first.
614          *
615          * @param post
616          *            The post the replies refer to
617          * @return The sorted list of replies for the post
618          */
619         public List<Reply> getReplies(Post post) {
620                 List<Reply> replies = new ArrayList<Reply>();
621                 for (Reply reply : replyCache.values()) {
622                         if (reply.getPost().equals(post)) {
623                                 replies.add(reply);
624                         }
625                 }
626                 Collections.sort(replies, new Comparator<Reply>() {
627
628                         /**
629                          * {@inheritDoc}
630                          */
631                         @Override
632                         public int compare(Reply leftReply, Reply rightReply) {
633                                 return (int) Math.max(Integer.MIN_VALUE, Math.min(Integer.MAX_VALUE, leftReply.getTime() - rightReply.getTime()));
634                         }
635                 });
636                 return replies;
637         }
638
639         /**
640          * Gets all Sones that like the given post.
641          *
642          * @param post
643          *            The post to check for
644          * @return All Sones that like the post
645          */
646         public Set<Sone> getLikes(final Post post) {
647                 return Filters.filteredSet(getSones(), new Filter<Sone>() {
648
649                         @Override
650                         public boolean filterObject(Sone sone) {
651                                 return sone.isLikedPostId(post.getId());
652                         }
653                 });
654         }
655
656         /**
657          * Gets all Sones that like the given reply.
658          *
659          * @param reply
660          *            The reply to check for
661          * @return All Sones that like the reply
662          */
663         public Set<Sone> getLikes(final Reply reply) {
664                 return Filters.filteredSet(getSones(), new Filter<Sone>() {
665
666                         @Override
667                         public boolean filterObject(Sone sone) {
668                                 return sone.isLikedReplyId(reply.getId());
669                         }
670                 });
671         }
672
673         /**
674          * Deletes the given reply. It is removed from its Sone and from the reply
675          * cache.
676          *
677          * @param reply
678          *            The reply to remove
679          */
680         public void deleteReply(Reply reply) {
681                 reply.getSone().removeReply(reply);
682                 replyCache.remove(reply.getId());
683         }
684
685         //
686         // SERVICE METHODS
687         //
688
689         /**
690          * {@inheritDoc}
691          */
692         @Override
693         protected void serviceStart() {
694                 loadConfiguration();
695         }
696
697         /**
698          * {@inheritDoc}
699          */
700         @Override
701         protected void serviceStop() {
702                 soneDownloader.stop();
703                 /* stop all Sone inserters. */
704                 for (SoneInserter soneInserter : soneInserters.values()) {
705                         soneInserter.stop();
706                 }
707                 saveConfiguration();
708         }
709
710         //
711         // PRIVATE METHODS
712         //
713
714         /**
715          * Adds some default Sones.
716          */
717         private void addDefaultSones() {
718                 /* Sone’s Sone. */
719                 loadSone("USK@eRHt0ceFsHjRZ11j6dd68RSdIvfd8f9YjJLZ9lnhEyo,iJWjIWh6TkMZm1NY8qBranKTIuwsCPkVPG6T6c6ft-I,AQACAAE/Sone/0");
720                 /* Bombe’s Sone. */
721                 loadSone("USK@RuW~uAO35Ipne896-1OmaVJNPuYE4ZIB5oZ5ziaU57A,7rV3uiyztXBDt03DCoRiNwiGjgFCJuznM9Okc1opURU,AQACAAE/Sone/15");
722         }
723
724         /**
725          * Loads the configuration.
726          */
727         @SuppressWarnings("unchecked")
728         private void loadConfiguration() {
729                 logger.entering(Core.class.getName(), "loadConfiguration()");
730
731                 boolean firstStart = configuration.getBooleanValue("FirstStart").getValue(true);
732                 if (firstStart) {
733                         logger.log(Level.INFO, "First start of Sone, adding a couple of default Sones…");
734                         addDefaultSones();
735                         try {
736                                 configuration.getBooleanValue("FirstStart").setValue(false);
737                         } catch (ConfigurationException ce1) {
738                                 logger.log(Level.WARNING, "Could not clear “first start” flag!");
739                         }
740                 }
741
742                 options.addIntegerOption("InsertionDelay", new DefaultOption<Integer>(60, new OptionWatcher<Integer>() {
743
744                         @Override
745                         public void optionChanged(Option<Integer> option, Integer oldValue, Integer newValue) {
746                                 SoneInserter.setInsertionDelay(newValue);
747                         }
748
749                 }));
750
751                 options.addBooleanOption("ClearOnNextRestart", new DefaultOption<Boolean>(false));
752                 options.addBooleanOption("ReallyClearOnNextRestart", new DefaultOption<Boolean>(false));
753
754                 if (firstStart) {
755                         return;
756                 }
757
758                 options.getBooleanOption("ClearOnNextRestart").set(configuration.getBooleanValue("Option/ClearOnNextRestart").getValue(null));
759                 options.getBooleanOption("ReallyClearOnNextRestart").set(configuration.getBooleanValue("Option/ReallyClearOnNextRestart").getValue(null));
760                 boolean clearConfiguration = options.getBooleanOption("ClearOnNextRestart").get() && options.getBooleanOption("ReallyClearOnNextRestart").get();
761                 options.getBooleanOption("ClearOnNextRestart").set(null);
762                 options.getBooleanOption("ReallyClearOnNextRestart").set(null);
763                 if (clearConfiguration) {
764                         /* stop loading the configuration. */
765                         addDefaultSones();
766                         return;
767                 }
768
769                 options.getIntegerOption("InsertionDelay").set(configuration.getIntValue("Option/InsertionDelay").getValue(null));
770
771                 /* parse local Sones. */
772                 logger.log(Level.INFO, "Loading Sones…");
773                 int soneId = 0;
774                 do {
775                         String sonePrefix = "Sone/Sone." + soneId++;
776                         String id = configuration.getStringValue(sonePrefix + "/ID").getValue(null);
777                         if (id == null) {
778                                 break;
779                         }
780                         String name = configuration.getStringValue(sonePrefix + "/Name").getValue(null);
781                         long time = configuration.getLongValue(sonePrefix + "/Time").getValue((long) 0);
782                         String insertUri = configuration.getStringValue(sonePrefix + "/InsertURI").getValue(null);
783                         String requestUri = configuration.getStringValue(sonePrefix + "/RequestURI").getValue(null);
784                         long modificationCounter = configuration.getLongValue(sonePrefix + "/ModificationCounter").getValue((long) 0);
785                         String firstName = configuration.getStringValue(sonePrefix + "/Profile/FirstName").getValue(null);
786                         String middleName = configuration.getStringValue(sonePrefix + "/Profile/MiddleName").getValue(null);
787                         String lastName = configuration.getStringValue(sonePrefix + "/Profile/LastName").getValue(null);
788                         Integer birthDay = configuration.getIntValue(sonePrefix + "/Profile/BirthDay").getValue(null);
789                         Integer birthMonth = configuration.getIntValue(sonePrefix + "/Profile/BirthMonth").getValue(null);
790                         Integer birthYear = configuration.getIntValue(sonePrefix + "/Profile/BirthYear").getValue(null);
791                         try {
792                                 Profile profile = new Profile();
793                                 profile.setFirstName(firstName);
794                                 profile.setMiddleName(middleName);
795                                 profile.setLastName(lastName);
796                                 profile.setBirthDay(birthDay).setBirthMonth(birthMonth).setBirthYear(birthYear);
797                                 Sone sone = getSone(id).setName(name).setTime(time).setRequestUri(new FreenetURI(requestUri)).setInsertUri(new FreenetURI(insertUri));
798                                 sone.setProfile(profile);
799                                 int postId = 0;
800                                 do {
801                                         String postPrefix = sonePrefix + "/Post." + postId++;
802                                         id = configuration.getStringValue(postPrefix + "/ID").getValue(null);
803                                         if (id == null) {
804                                                 break;
805                                         }
806                                         time = configuration.getLongValue(postPrefix + "/Time").getValue((long) 0);
807                                         String text = configuration.getStringValue(postPrefix + "/Text").getValue(null);
808                                         Post post = getPost(id).setSone(sone).setTime(time).setText(text);
809                                         sone.addPost(post);
810                                 } while (true);
811                                 int replyCounter = 0;
812                                 do {
813                                         String replyPrefix = sonePrefix + "/Reply." + replyCounter++;
814                                         String replyId = configuration.getStringValue(replyPrefix + "/ID").getValue(null);
815                                         if (replyId == null) {
816                                                 break;
817                                         }
818                                         Post replyPost = getPost(configuration.getStringValue(replyPrefix + "/Post").getValue(null));
819                                         long replyTime = configuration.getLongValue(replyPrefix + "/Time").getValue(null);
820                                         String replyText = configuration.getStringValue(replyPrefix + "/Text").getValue(null);
821                                         Reply reply = getReply(replyId).setSone(sone).setPost(replyPost).setTime(replyTime).setText(replyText);
822                                         sone.addReply(reply);
823                                 } while (true);
824
825                                 /* load friends. */
826                                 int friendCounter = 0;
827                                 while (true) {
828                                         String friendPrefix = sonePrefix + "/Friend." + friendCounter++;
829                                         String friendId = configuration.getStringValue(friendPrefix + "/ID").getValue(null);
830                                         if (friendId == null) {
831                                                 break;
832                                         }
833                                         Sone friendSone = getSone(friendId);
834                                         String friendKey = configuration.getStringValue(friendPrefix + "/Key").getValue(null);
835                                         String friendName = configuration.getStringValue(friendPrefix + "/Name").getValue(null);
836                                         friendSone.setRequestUri(new FreenetURI(friendKey)).setName(friendName);
837                                         sone.addFriend(friendSone);
838                                 }
839
840                                 /* load blocked Sone IDs. */
841                                 int blockedSoneCounter = 0;
842                                 while (true) {
843                                         String blockedSonePrefix = sonePrefix + "/BlockedSone." + blockedSoneCounter++;
844                                         String blockedSoneId = configuration.getStringValue(blockedSonePrefix + "/ID").getValue(null);
845                                         if (blockedSoneId == null) {
846                                                 break;
847                                         }
848                                         sone.addBlockedSoneId(blockedSoneId);
849                                 }
850
851                                 /* load liked post IDs. */
852                                 int likedPostIdCounter = 0;
853                                 while (true) {
854                                         String likedPostIdPrefix = sonePrefix + "/LikedPostId." + likedPostIdCounter++;
855                                         String likedPostId = configuration.getStringValue(likedPostIdPrefix + "/ID").getValue(null);
856                                         if (likedPostId == null) {
857                                                 break;
858                                         }
859                                         sone.addLikedPostId(likedPostId);
860                                 }
861
862                                 /* load liked reply IDs. */
863                                 int likedReplyIdCounter = 0;
864                                 while (true) {
865                                         String likedReplyIdPrefix = sonePrefix + "/LikedReplyId." + likedReplyIdCounter++;
866                                         String likedReplyId = configuration.getStringValue(likedReplyIdPrefix + "/ID").getValue(null);
867                                         if (likedReplyId == null) {
868                                                 break;
869                                         }
870                                         sone.addLikedReplyId(likedReplyId);
871                                 }
872
873                                 sone.setModificationCounter(modificationCounter);
874                                 addLocalSone(sone);
875                         } catch (MalformedURLException mue1) {
876                                 logger.log(Level.WARNING, "Could not create Sone from requestUri (“" + requestUri + "”) and insertUri (“" + insertUri + "”)!", mue1);
877                         }
878                 } while (true);
879                 logger.log(Level.INFO, "Loaded %d Sones.", getSones().size());
880
881                 /* load all known Sones. */
882                 int knownSonesCounter = 0;
883                 while (true) {
884                         String knownSonePrefix = "KnownSone." + knownSonesCounter++;
885                         String knownSoneId = configuration.getStringValue(knownSonePrefix + "/ID").getValue(null);
886                         if (knownSoneId == null) {
887                                 break;
888                         }
889                         String knownSoneName = configuration.getStringValue(knownSonePrefix + "/Name").getValue(null);
890                         String knownSoneKey = configuration.getStringValue(knownSonePrefix + "/Key").getValue(null);
891                         try {
892                                 getSone(knownSoneId).setName(knownSoneName).setRequestUri(new FreenetURI(knownSoneKey));
893                         } catch (MalformedURLException mue1) {
894                                 logger.log(Level.WARNING, "Could not create Sone from requestUri (“" + knownSoneKey + "”)!", mue1);
895                         }
896                 }
897
898                 /* load all blacklisted Sones. */
899                 int blacklistedSonesCounter = 0;
900                 while (true) {
901                         String blacklistedSonePrefix = "BlacklistedSone." + blacklistedSonesCounter++;
902                         String blacklistedSoneId = configuration.getStringValue(blacklistedSonePrefix + "/ID").getValue(null);
903                         if (blacklistedSoneId == null) {
904                                 break;
905                         }
906                         String blacklistedSoneName = configuration.getStringValue(blacklistedSonePrefix + "/Name").getValue(null);
907                         String blacklistedSoneKey = configuration.getStringValue(blacklistedSonePrefix + "/Key").getValue(null);
908                         String blacklistedSoneInsertKey = configuration.getStringValue(blacklistedSonePrefix + "/InsertKey").getValue(null);
909                         try {
910                                 blacklistSone(getSone(blacklistedSoneId).setName(blacklistedSoneName).setRequestUri(new FreenetURI(blacklistedSoneKey)).setInsertUri((blacklistedSoneInsertKey != null) ? new FreenetURI(blacklistedSoneInsertKey) : null));
911                         } catch (MalformedURLException mue1) {
912                                 logger.log(Level.WARNING, "Could not create blacklisted Sone from requestUri (“" + blacklistedSoneKey + "”)!", mue1);
913                         }
914                 }
915
916                 /* load all remote Sones. */
917                 for (Sone remoteSone : getRemoteSones()) {
918                         loadSone(remoteSone);
919                 }
920
921                 logger.exiting(Core.class.getName(), "loadConfiguration()");
922         }
923
924         /**
925          * Saves the configuraiton.
926          */
927         private void saveConfiguration() {
928                 Set<Sone> sones = getSones();
929                 logger.log(Level.INFO, "Storing %d Sones…", sones.size());
930
931                 try {
932                         /* store the options first. */
933                         configuration.getIntValue("Option/InsertionDelay").setValue(options.getIntegerOption("InsertionDelay").getReal());
934                         configuration.getBooleanValue("Option/ClearOnNextRestart").setValue(options.getBooleanOption("ClearOnNextRestart").getReal());
935                         configuration.getBooleanValue("Option/ReallyClearOnNextRestart").setValue(options.getBooleanOption("ReallyClearOnNextRestart").getReal());
936
937                         /* store all Sones. */
938                         int soneId = 0;
939                         for (Sone sone : localSones) {
940                                 String sonePrefix = "Sone/Sone." + soneId++;
941                                 configuration.getStringValue(sonePrefix + "/ID").setValue(sone.getId());
942                                 configuration.getStringValue(sonePrefix + "/Name").setValue(sone.getName());
943                                 configuration.getLongValue(sonePrefix + "/Time").setValue(sone.getTime());
944                                 configuration.getStringValue(sonePrefix + "/RequestURI").setValue(sone.getRequestUri().toString());
945                                 configuration.getStringValue(sonePrefix + "/InsertURI").setValue(sone.getInsertUri().toString());
946                                 configuration.getLongValue(sonePrefix + "/ModificationCounter").setValue(sone.getModificationCounter());
947                                 Profile profile = sone.getProfile();
948                                 configuration.getStringValue(sonePrefix + "/Profile/FirstName").setValue(profile.getFirstName());
949                                 configuration.getStringValue(sonePrefix + "/Profile/MiddleName").setValue(profile.getMiddleName());
950                                 configuration.getStringValue(sonePrefix + "/Profile/LastName").setValue(profile.getLastName());
951                                 configuration.getIntValue(sonePrefix + "/Profile/BirthDay").setValue(profile.getBirthDay());
952                                 configuration.getIntValue(sonePrefix + "/Profile/BirthMonth").setValue(profile.getBirthMonth());
953                                 configuration.getIntValue(sonePrefix + "/Profile/BirthYear").setValue(profile.getBirthYear());
954                                 int postId = 0;
955                                 for (Post post : sone.getPosts()) {
956                                         String postPrefix = sonePrefix + "/Post." + postId++;
957                                         configuration.getStringValue(postPrefix + "/ID").setValue(post.getId());
958                                         configuration.getLongValue(postPrefix + "/Time").setValue(post.getTime());
959                                         configuration.getStringValue(postPrefix + "/Text").setValue(post.getText());
960                                 }
961                                 /* write null ID as terminator. */
962                                 configuration.getStringValue(sonePrefix + "/Post." + postId + "/ID").setValue(null);
963
964                                 int replyId = 0;
965                                 for (Reply reply : sone.getReplies()) {
966                                         String replyPrefix = sonePrefix + "/Reply." + replyId++;
967                                         configuration.getStringValue(replyPrefix + "/ID").setValue(reply.getId());
968                                         configuration.getStringValue(replyPrefix + "/Post").setValue(reply.getPost().getId());
969                                         configuration.getLongValue(replyPrefix + "/Time").setValue(reply.getTime());
970                                         configuration.getStringValue(replyPrefix + "/Text").setValue(reply.getText());
971                                 }
972                                 /* write null ID as terminator. */
973                                 configuration.getStringValue(sonePrefix + "/Reply." + replyId + "/ID").setValue(null);
974
975                                 int friendId = 0;
976                                 for (Sone friend : sone.getFriends()) {
977                                         String friendPrefix = sonePrefix + "/Friend." + friendId++;
978                                         configuration.getStringValue(friendPrefix + "/ID").setValue(friend.getId());
979                                         configuration.getStringValue(friendPrefix + "/Key").setValue(friend.getRequestUri().toString());
980                                         configuration.getStringValue(friendPrefix + "/Name").setValue(friend.getName());
981                                 }
982                                 /* write null ID as terminator. */
983                                 configuration.getStringValue(sonePrefix + "/Friend." + friendId + "/ID").setValue(null);
984
985                                 /* write all blocked Sones. */
986                                 int blockedSoneCounter = 0;
987                                 for (String blockedSoneId : sone.getBlockedSoneIds()) {
988                                         String blockedSonePrefix = sonePrefix + "/BlockedSone." + blockedSoneCounter++;
989                                         configuration.getStringValue(blockedSonePrefix + "/ID").setValue(blockedSoneId);
990                                 }
991                                 configuration.getStringValue(sonePrefix + "/BlockedSone." + blockedSoneCounter + "/ID").setValue(null);
992
993                                 /* write all liked posts. */
994                                 int likedPostIdCounter = 0;
995                                 for (String soneLikedPostId : sone.getLikedPostIds()) {
996                                         String likedPostIdPrefix = sonePrefix + "/LikedPostId." + likedPostIdCounter++;
997                                         configuration.getStringValue(likedPostIdPrefix + "/ID").setValue(soneLikedPostId);
998                                 }
999                                 configuration.getStringValue(sonePrefix + "/LikedPostId." + likedPostIdCounter + "/ID").setValue(null);
1000
1001                                 /* write all liked replies. */
1002                                 int likedReplyIdCounter = 0;
1003                                 for (String soneLikedReplyId : sone.getLikedReplyIds()) {
1004                                         String likedReplyIdPrefix = sonePrefix + "/LikedReplyId." + likedReplyIdCounter++;
1005                                         configuration.getStringValue(likedReplyIdPrefix + "/ID").setValue(soneLikedReplyId);
1006                                 }
1007                                 configuration.getStringValue(sonePrefix + "/LikedReplyId." + likedReplyIdCounter + "/ID").setValue(null);
1008
1009                         }
1010                         /* write null ID as terminator. */
1011                         configuration.getStringValue("Sone/Sone." + soneId + "/ID").setValue(null);
1012
1013                         /* write all known Sones. */
1014                         int knownSonesCounter = 0;
1015                         for (Sone knownSone : getRemoteSones()) {
1016                                 String knownSonePrefix = "KnownSone." + knownSonesCounter++;
1017                                 configuration.getStringValue(knownSonePrefix + "/ID").setValue(knownSone.getId());
1018                                 configuration.getStringValue(knownSonePrefix + "/Name").setValue(knownSone.getName());
1019                                 configuration.getStringValue(knownSonePrefix + "/Key").setValue(knownSone.getRequestUri().toString());
1020                                 /* TODO - store all known stuff? */
1021                         }
1022                         configuration.getStringValue("KnownSone." + knownSonesCounter + "/ID").setValue(null);
1023
1024                         /* write all blacklisted Sones. */
1025                         int blacklistedSonesCounter = 0;
1026                         for (Sone blacklistedSone : getBlacklistedSones()) {
1027                                 String blacklistedSonePrefix = "BlacklistedSone." + blacklistedSonesCounter++;
1028                                 configuration.getStringValue(blacklistedSonePrefix + "/ID").setValue(blacklistedSone.getId());
1029                                 configuration.getStringValue(blacklistedSonePrefix + "/Name").setValue(blacklistedSone.getName());
1030                                 configuration.getStringValue(blacklistedSonePrefix + "/Key").setValue(blacklistedSone.getRequestUri().toString());
1031                                 configuration.getStringValue(blacklistedSonePrefix + "/InsertKey").setValue((blacklistedSone.getInsertUri() != null) ? blacklistedSone.getInsertUri().toString() : null);
1032                                 /* TODO - store all known stuff? */
1033                         }
1034                         configuration.getStringValue("BlacklistedSone." + blacklistedSonesCounter + "/ID").setValue(null);
1035
1036                 } catch (ConfigurationException ce1) {
1037                         logger.log(Level.WARNING, "Could not store configuration!", ce1);
1038                 }
1039         }
1040
1041 }