Store insert key of blacklisted Sone correctly.
[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                                         }
509                                 } catch (MalformedURLException mue1) {
510                                         logger.log(Level.INFO, "Could not create URI from “" + requestUri + "”.", mue1);
511                                 }
512                         }
513                 }, "Sone Downloader").start();
514         }
515
516         /**
517          * Loads a Sone from an input stream.
518          *
519          * @param soneInputStream
520          *            The input stream to load the Sone from
521          * @return The parsed Sone, or {@code null} if the Sone could not be parsed
522          */
523         public Sone loadSone(InputStream soneInputStream) {
524                 Sone parsedSone = soneDownloader.parseSone(soneInputStream);
525                 if (parsedSone == null) {
526                         return null;
527                 }
528                 if (parsedSone.getInsertUri() != null) {
529                         addLocalSone(parsedSone);
530                 } else {
531                         addSone(parsedSone);
532                 }
533                 return parsedSone;
534         }
535
536         /**
537          * Loads and updates the given Sone.
538          *
539          * @param sone
540          *            The Sone to load
541          */
542         public void loadSone(final Sone sone) {
543                 new Thread(new Runnable() {
544
545                         @Override
546                         @SuppressWarnings("synthetic-access")
547                         public void run() {
548                                 FreenetURI realRequestUri = sone.getRequestUri().setMetaString(new String[] { "sone.xml" });
549                                 setSoneStatus(sone, SoneStatus.downloading);
550                                 try {
551                                         FetchResult fetchResult = freenetInterface.fetchUri(realRequestUri);
552                                         if (fetchResult == null) {
553                                                 /* TODO - mark Sone as bad. */
554                                                 return;
555                                         }
556                                         Sone parsedSone = soneDownloader.parseSone(sone, fetchResult, realRequestUri);
557                                         if (parsedSone != null) {
558                                                 addSone(parsedSone);
559                                         }
560                                 } finally {
561                                         setSoneStatus(sone, (sone.getTime() == 0) ? SoneStatus.unknown : SoneStatus.idle);
562                                 }
563                         }
564                 }, "Sone Downloader").start();
565         }
566
567         /**
568          * Deletes the given Sone from this plugin instance.
569          *
570          * @param sone
571          *            The sone to delete
572          */
573         public void deleteSone(Sone sone) {
574                 SoneInserter soneInserter = soneInserters.remove(sone);
575                 soneInserter.stop();
576                 localSones.remove(sone);
577                 soneStatuses.remove(sone);
578                 soneCache.remove(sone.getId());
579         }
580
581         /**
582          * Returns the post with the given ID. If no post exists yet with the given
583          * ID, a new post is returned.
584          *
585          * @param postId
586          *            The ID of the post
587          * @return The post
588          */
589         public Post getPost(String postId) {
590                 if (!postCache.containsKey(postId)) {
591                         postCache.put(postId, new Post(postId));
592                 }
593                 return postCache.get(postId);
594         }
595
596         /**
597          * Returns the reply with the given ID. If no reply exists yet with the
598          * given ID, a new reply is returned.
599          *
600          * @param replyId
601          *            The ID of the reply
602          * @return The reply
603          */
604         public Reply getReply(String replyId) {
605                 if (!replyCache.containsKey(replyId)) {
606                         replyCache.put(replyId, new Reply(replyId));
607                 }
608                 return replyCache.get(replyId);
609         }
610
611         /**
612          * Gets all replies to the given post, sorted by date, oldest first.
613          *
614          * @param post
615          *            The post the replies refer to
616          * @return The sorted list of replies for the post
617          */
618         public List<Reply> getReplies(Post post) {
619                 List<Reply> replies = new ArrayList<Reply>();
620                 for (Reply reply : replyCache.values()) {
621                         if (reply.getPost().equals(post)) {
622                                 replies.add(reply);
623                         }
624                 }
625                 Collections.sort(replies, new Comparator<Reply>() {
626
627                         /**
628                          * {@inheritDoc}
629                          */
630                         @Override
631                         public int compare(Reply leftReply, Reply rightReply) {
632                                 return (int) Math.max(Integer.MIN_VALUE, Math.min(Integer.MAX_VALUE, leftReply.getTime() - rightReply.getTime()));
633                         }
634                 });
635                 return replies;
636         }
637
638         /**
639          * Gets all Sones that like the given post.
640          *
641          * @param post
642          *            The post to check for
643          * @return All Sones that like the post
644          */
645         public Set<Sone> getLikes(final Post post) {
646                 return Filters.filteredSet(getSones(), new Filter<Sone>() {
647
648                         @Override
649                         public boolean filterObject(Sone sone) {
650                                 return sone.isLikedPostId(post.getId());
651                         }
652                 });
653         }
654
655         /**
656          * Gets all Sones that like the given reply.
657          *
658          * @param reply
659          *            The reply to check for
660          * @return All Sones that like the reply
661          */
662         public Set<Sone> getLikes(final Reply reply) {
663                 return Filters.filteredSet(getSones(), new Filter<Sone>() {
664
665                         @Override
666                         public boolean filterObject(Sone sone) {
667                                 return sone.isLikedReplyId(reply.getId());
668                         }
669                 });
670         }
671
672         /**
673          * Deletes the given reply. It is removed from its Sone and from the reply
674          * cache.
675          *
676          * @param reply
677          *            The reply to remove
678          */
679         public void deleteReply(Reply reply) {
680                 reply.getSone().removeReply(reply);
681                 replyCache.remove(reply.getId());
682         }
683
684         //
685         // SERVICE METHODS
686         //
687
688         /**
689          * {@inheritDoc}
690          */
691         @Override
692         protected void serviceStart() {
693                 loadConfiguration();
694         }
695
696         /**
697          * {@inheritDoc}
698          */
699         @Override
700         protected void serviceStop() {
701                 soneDownloader.stop();
702                 /* stop all Sone inserters. */
703                 for (SoneInserter soneInserter : soneInserters.values()) {
704                         soneInserter.stop();
705                 }
706                 saveConfiguration();
707         }
708
709         //
710         // PRIVATE METHODS
711         //
712
713         /**
714          * Loads the configuration.
715          */
716         @SuppressWarnings("unchecked")
717         private void loadConfiguration() {
718                 logger.entering(Core.class.getName(), "loadConfiguration()");
719
720                 options.addIntegerOption("InsertionDelay", new DefaultOption<Integer>(60, new OptionWatcher<Integer>() {
721
722                         @Override
723                         public void optionChanged(Option<Integer> option, Integer oldValue, Integer newValue) {
724                                 SoneInserter.setInsertionDelay(newValue);
725                         }
726
727                 }));
728
729                 options.addBooleanOption("ClearOnNextRestart", new DefaultOption<Boolean>(false)).set(configuration.getBooleanValue("Option/ClearOnNextRestart").getValue(null));
730                 options.addBooleanOption("ReallyClearOnNextRestart", new DefaultOption<Boolean>(false)).set(configuration.getBooleanValue("Option/ReallyClearOnNextRestart").getValue(null));
731
732                 boolean clearConfiguration = options.getBooleanOption("ClearOnNextRestart").get() && options.getBooleanOption("ReallyClearOnNextRestart").get();
733                 options.getBooleanOption("ClearOnNextRestart").set(null);
734                 options.getBooleanOption("ReallyClearOnNextRestart").set(null);
735                 if (clearConfiguration) {
736                         /* stop loading the configuration. */
737                         return;
738                 }
739
740                 options.getIntegerOption("InsertionDelay").set(configuration.getIntValue("Option/InsertionDelay").getValue(null));
741
742                 /* parse local Sones. */
743                 logger.log(Level.INFO, "Loading Sones…");
744                 int soneId = 0;
745                 do {
746                         String sonePrefix = "Sone/Sone." + soneId++;
747                         String id = configuration.getStringValue(sonePrefix + "/ID").getValue(null);
748                         if (id == null) {
749                                 break;
750                         }
751                         String name = configuration.getStringValue(sonePrefix + "/Name").getValue(null);
752                         long time = configuration.getLongValue(sonePrefix + "/Time").getValue((long) 0);
753                         String insertUri = configuration.getStringValue(sonePrefix + "/InsertURI").getValue(null);
754                         String requestUri = configuration.getStringValue(sonePrefix + "/RequestURI").getValue(null);
755                         long modificationCounter = configuration.getLongValue(sonePrefix + "/ModificationCounter").getValue((long) 0);
756                         String firstName = configuration.getStringValue(sonePrefix + "/Profile/FirstName").getValue(null);
757                         String middleName = configuration.getStringValue(sonePrefix + "/Profile/MiddleName").getValue(null);
758                         String lastName = configuration.getStringValue(sonePrefix + "/Profile/LastName").getValue(null);
759                         Integer birthDay = configuration.getIntValue(sonePrefix + "/Profile/BirthDay").getValue(null);
760                         Integer birthMonth = configuration.getIntValue(sonePrefix + "/Profile/BirthMonth").getValue(null);
761                         Integer birthYear = configuration.getIntValue(sonePrefix + "/Profile/BirthYear").getValue(null);
762                         try {
763                                 Profile profile = new Profile();
764                                 profile.setFirstName(firstName);
765                                 profile.setMiddleName(middleName);
766                                 profile.setLastName(lastName);
767                                 profile.setBirthDay(birthDay).setBirthMonth(birthMonth).setBirthYear(birthYear);
768                                 Sone sone = getSone(id).setName(name).setTime(time).setRequestUri(new FreenetURI(requestUri)).setInsertUri(new FreenetURI(insertUri));
769                                 sone.setProfile(profile);
770                                 int postId = 0;
771                                 do {
772                                         String postPrefix = sonePrefix + "/Post." + postId++;
773                                         id = configuration.getStringValue(postPrefix + "/ID").getValue(null);
774                                         if (id == null) {
775                                                 break;
776                                         }
777                                         time = configuration.getLongValue(postPrefix + "/Time").getValue((long) 0);
778                                         String text = configuration.getStringValue(postPrefix + "/Text").getValue(null);
779                                         Post post = getPost(id).setSone(sone).setTime(time).setText(text);
780                                         sone.addPost(post);
781                                 } while (true);
782                                 int replyCounter = 0;
783                                 do {
784                                         String replyPrefix = sonePrefix + "/Reply." + replyCounter++;
785                                         String replyId = configuration.getStringValue(replyPrefix + "/ID").getValue(null);
786                                         if (replyId == null) {
787                                                 break;
788                                         }
789                                         Post replyPost = getPost(configuration.getStringValue(replyPrefix + "/Post").getValue(null));
790                                         long replyTime = configuration.getLongValue(replyPrefix + "/Time").getValue(null);
791                                         String replyText = configuration.getStringValue(replyPrefix + "/Text").getValue(null);
792                                         Reply reply = getReply(replyId).setSone(sone).setPost(replyPost).setTime(replyTime).setText(replyText);
793                                         sone.addReply(reply);
794                                 } while (true);
795
796                                 /* load friends. */
797                                 int friendCounter = 0;
798                                 while (true) {
799                                         String friendPrefix = sonePrefix + "/Friend." + friendCounter++;
800                                         String friendId = configuration.getStringValue(friendPrefix + "/ID").getValue(null);
801                                         if (friendId == null) {
802                                                 break;
803                                         }
804                                         Sone friendSone = getSone(friendId);
805                                         String friendKey = configuration.getStringValue(friendPrefix + "/Key").getValue(null);
806                                         String friendName = configuration.getStringValue(friendPrefix + "/Name").getValue(null);
807                                         friendSone.setRequestUri(new FreenetURI(friendKey)).setName(friendName);
808                                         sone.addFriend(friendSone);
809                                 }
810
811                                 /* load blocked Sone IDs. */
812                                 int blockedSoneCounter = 0;
813                                 while (true) {
814                                         String blockedSonePrefix = sonePrefix + "/BlockedSone." + blockedSoneCounter++;
815                                         String blockedSoneId = configuration.getStringValue(blockedSonePrefix + "/ID").getValue(null);
816                                         if (blockedSoneId == null) {
817                                                 break;
818                                         }
819                                         sone.addBlockedSoneId(blockedSoneId);
820                                 }
821
822                                 /* load liked post IDs. */
823                                 int likedPostIdCounter = 0;
824                                 while (true) {
825                                         String likedPostIdPrefix = sonePrefix + "/LikedPostId." + likedPostIdCounter++;
826                                         String likedPostId = configuration.getStringValue(likedPostIdPrefix + "/ID").getValue(null);
827                                         if (likedPostId == null) {
828                                                 break;
829                                         }
830                                         sone.addLikedPostId(likedPostId);
831                                 }
832
833                                 /* load liked reply IDs. */
834                                 int likedReplyIdCounter = 0;
835                                 while (true) {
836                                         String likedReplyIdPrefix = sonePrefix + "/LikedReplyId." + likedReplyIdCounter++;
837                                         String likedReplyId = configuration.getStringValue(likedReplyIdPrefix + "/ID").getValue(null);
838                                         if (likedReplyId == null) {
839                                                 break;
840                                         }
841                                         sone.addLikedReplyId(likedReplyId);
842                                 }
843
844                                 sone.setModificationCounter(modificationCounter);
845                                 addLocalSone(sone);
846                         } catch (MalformedURLException mue1) {
847                                 logger.log(Level.WARNING, "Could not create Sone from requestUri (“" + requestUri + "”) and insertUri (“" + insertUri + "”)!", mue1);
848                         }
849                 } while (true);
850                 logger.log(Level.INFO, "Loaded %d Sones.", getSones().size());
851
852                 /* load all known Sones. */
853                 int knownSonesCounter = 0;
854                 while (true) {
855                         String knownSonePrefix = "KnownSone." + knownSonesCounter++;
856                         String knownSoneId = configuration.getStringValue(knownSonePrefix + "/ID").getValue(null);
857                         if (knownSoneId == null) {
858                                 break;
859                         }
860                         String knownSoneName = configuration.getStringValue(knownSonePrefix + "/Name").getValue(null);
861                         String knownSoneKey = configuration.getStringValue(knownSonePrefix + "/Key").getValue(null);
862                         try {
863                                 getSone(knownSoneId).setName(knownSoneName).setRequestUri(new FreenetURI(knownSoneKey));
864                         } catch (MalformedURLException mue1) {
865                                 logger.log(Level.WARNING, "Could not create Sone from requestUri (“" + knownSoneKey + "”)!", mue1);
866                         }
867                 }
868
869                 /* load all blacklisted Sones. */
870                 int blacklistedSonesCounter = 0;
871                 while (true) {
872                         String blacklistedSonePrefix = "BlacklistedSone." + blacklistedSonesCounter++;
873                         String blacklistedSoneId = configuration.getStringValue(blacklistedSonePrefix + "/ID").getValue(null);
874                         if (blacklistedSoneId == null) {
875                                 break;
876                         }
877                         String blacklistedSoneName = configuration.getStringValue(blacklistedSonePrefix + "/Name").getValue(null);
878                         String blacklistedSoneKey = configuration.getStringValue(blacklistedSonePrefix + "/Key").getValue(null);
879                         String blacklistedSoneInsertKey = configuration.getStringValue(blacklistedSonePrefix + "/InsertKey").getValue(null);
880                         try {
881                                 blacklistSone(getSone(blacklistedSoneId).setName(blacklistedSoneName).setRequestUri(new FreenetURI(blacklistedSoneKey)).setInsertUri((blacklistedSoneInsertKey != null) ? new FreenetURI(blacklistedSoneInsertKey) : null));
882                         } catch (MalformedURLException mue1) {
883                                 logger.log(Level.WARNING, "Could not create blacklisted Sone from requestUri (“" + blacklistedSoneKey + "”)!", mue1);
884                         }
885                 }
886
887                 /* load all remote Sones. */
888                 for (Sone remoteSone : getRemoteSones()) {
889                         loadSone(remoteSone);
890                 }
891
892                 logger.exiting(Core.class.getName(), "loadConfiguration()");
893         }
894
895         /**
896          * Saves the configuraiton.
897          */
898         private void saveConfiguration() {
899                 Set<Sone> sones = getSones();
900                 logger.log(Level.INFO, "Storing %d Sones…", sones.size());
901
902                 try {
903                         /* store the options first. */
904                         configuration.getIntValue("Option/InsertionDelay").setValue(options.getIntegerOption("InsertionDelay").getReal());
905                         configuration.getBooleanValue("Option/ClearOnNextRestart").setValue(options.getBooleanOption("ClearOnNextRestart").getReal());
906                         configuration.getBooleanValue("Option/ReallyClearOnNextRestart").setValue(options.getBooleanOption("ReallyClearOnNextRestart").getReal());
907
908                         /* store all Sones. */
909                         int soneId = 0;
910                         for (Sone sone : localSones) {
911                                 String sonePrefix = "Sone/Sone." + soneId++;
912                                 configuration.getStringValue(sonePrefix + "/ID").setValue(sone.getId());
913                                 configuration.getStringValue(sonePrefix + "/Name").setValue(sone.getName());
914                                 configuration.getLongValue(sonePrefix + "/Time").setValue(sone.getTime());
915                                 configuration.getStringValue(sonePrefix + "/RequestURI").setValue(sone.getRequestUri().toString());
916                                 configuration.getStringValue(sonePrefix + "/InsertURI").setValue(sone.getInsertUri().toString());
917                                 configuration.getLongValue(sonePrefix + "/ModificationCounter").setValue(sone.getModificationCounter());
918                                 Profile profile = sone.getProfile();
919                                 configuration.getStringValue(sonePrefix + "/Profile/FirstName").setValue(profile.getFirstName());
920                                 configuration.getStringValue(sonePrefix + "/Profile/MiddleName").setValue(profile.getMiddleName());
921                                 configuration.getStringValue(sonePrefix + "/Profile/LastName").setValue(profile.getLastName());
922                                 configuration.getIntValue(sonePrefix + "/Profile/BirthDay").setValue(profile.getBirthDay());
923                                 configuration.getIntValue(sonePrefix + "/Profile/BirthMonth").setValue(profile.getBirthMonth());
924                                 configuration.getIntValue(sonePrefix + "/Profile/BirthYear").setValue(profile.getBirthYear());
925                                 int postId = 0;
926                                 for (Post post : sone.getPosts()) {
927                                         String postPrefix = sonePrefix + "/Post." + postId++;
928                                         configuration.getStringValue(postPrefix + "/ID").setValue(post.getId());
929                                         configuration.getLongValue(postPrefix + "/Time").setValue(post.getTime());
930                                         configuration.getStringValue(postPrefix + "/Text").setValue(post.getText());
931                                 }
932                                 /* write null ID as terminator. */
933                                 configuration.getStringValue(sonePrefix + "/Post." + postId + "/ID").setValue(null);
934
935                                 int replyId = 0;
936                                 for (Reply reply : sone.getReplies()) {
937                                         String replyPrefix = sonePrefix + "/Reply." + replyId++;
938                                         configuration.getStringValue(replyPrefix + "/ID").setValue(reply.getId());
939                                         configuration.getStringValue(replyPrefix + "/Post").setValue(reply.getPost().getId());
940                                         configuration.getLongValue(replyPrefix + "/Time").setValue(reply.getTime());
941                                         configuration.getStringValue(replyPrefix + "/Text").setValue(reply.getText());
942                                 }
943                                 /* write null ID as terminator. */
944                                 configuration.getStringValue(sonePrefix + "/Reply." + replyId + "/ID").setValue(null);
945
946                                 int friendId = 0;
947                                 for (Sone friend : sone.getFriends()) {
948                                         String friendPrefix = sonePrefix + "/Friend." + friendId++;
949                                         configuration.getStringValue(friendPrefix + "/ID").setValue(friend.getId());
950                                         configuration.getStringValue(friendPrefix + "/Key").setValue(friend.getRequestUri().toString());
951                                         configuration.getStringValue(friendPrefix + "/Name").setValue(friend.getName());
952                                 }
953                                 /* write null ID as terminator. */
954                                 configuration.getStringValue(sonePrefix + "/Friend." + friendId + "/ID").setValue(null);
955
956                                 /* write all blocked Sones. */
957                                 int blockedSoneCounter = 0;
958                                 for (String blockedSoneId : sone.getBlockedSoneIds()) {
959                                         String blockedSonePrefix = sonePrefix + "/BlockedSone." + blockedSoneCounter++;
960                                         configuration.getStringValue(blockedSonePrefix + "/ID").setValue(blockedSoneId);
961                                 }
962                                 configuration.getStringValue(sonePrefix + "/BlockedSone." + blockedSoneCounter + "/ID").setValue(null);
963
964                                 /* write all liked posts. */
965                                 int likedPostIdCounter = 0;
966                                 for (String soneLikedPostId : sone.getLikedPostIds()) {
967                                         String likedPostIdPrefix = sonePrefix + "/LikedPostId." + likedPostIdCounter++;
968                                         configuration.getStringValue(likedPostIdPrefix + "/ID").setValue(soneLikedPostId);
969                                 }
970                                 configuration.getStringValue(sonePrefix + "/LikedPostId." + likedPostIdCounter + "/ID").setValue(null);
971
972                                 /* write all liked replies. */
973                                 int likedReplyIdCounter = 0;
974                                 for (String soneLikedReplyId : sone.getLikedReplyIds()) {
975                                         String likedReplyIdPrefix = sonePrefix + "/LikedReplyId." + likedReplyIdCounter++;
976                                         configuration.getStringValue(likedReplyIdPrefix + "/ID").setValue(soneLikedReplyId);
977                                 }
978                                 configuration.getStringValue(sonePrefix + "/LikedReplyId." + likedReplyIdCounter + "/ID").setValue(null);
979
980                         }
981                         /* write null ID as terminator. */
982                         configuration.getStringValue("Sone/Sone." + soneId + "/ID").setValue(null);
983
984                         /* write all known Sones. */
985                         int knownSonesCounter = 0;
986                         for (Sone knownSone : getRemoteSones()) {
987                                 String knownSonePrefix = "KnownSone." + knownSonesCounter++;
988                                 configuration.getStringValue(knownSonePrefix + "/ID").setValue(knownSone.getId());
989                                 configuration.getStringValue(knownSonePrefix + "/Name").setValue(knownSone.getName());
990                                 configuration.getStringValue(knownSonePrefix + "/Key").setValue(knownSone.getRequestUri().toString());
991                                 /* TODO - store all known stuff? */
992                         }
993                         configuration.getStringValue("KnownSone." + knownSonesCounter + "/ID").setValue(null);
994
995                         /* write all blacklisted Sones. */
996                         int blacklistedSonesCounter = 0;
997                         for (Sone blacklistedSone : getBlacklistedSones()) {
998                                 String blacklistedSonePrefix = "BlacklistedSone." + blacklistedSonesCounter++;
999                                 configuration.getStringValue(blacklistedSonePrefix + "/ID").setValue(blacklistedSone.getId());
1000                                 configuration.getStringValue(blacklistedSonePrefix + "/Name").setValue(blacklistedSone.getName());
1001                                 configuration.getStringValue(blacklistedSonePrefix + "/Key").setValue(blacklistedSone.getRequestUri().toString());
1002                                 configuration.getStringValue(blacklistedSonePrefix + "/InsertKey").setValue((blacklistedSone.getInsertUri() != null) ? blacklistedSone.getInsertUri().toString() : null);
1003                                 /* TODO - store all known stuff? */
1004                         }
1005                         configuration.getStringValue("BlacklistedSone." + blacklistedSonesCounter + "/ID").setValue(null);
1006
1007                 } catch (ConfigurationException ce1) {
1008                         logger.log(Level.WARNING, "Could not store configuration!", ce1);
1009                 }
1010         }
1011
1012 }