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