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