Add “first start” logic that adds a couple of default 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.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                 boolean firstStart = configuration.getBooleanValue("FirstStart").getValue(true);
721                 if (firstStart) {
722                         logger.log(Level.INFO, "First start of Sone, adding a couple of default Sones…");
723                         /* Sone’s Sone. */
724                         loadSone("USK@eRHt0ceFsHjRZ11j6dd68RSdIvfd8f9YjJLZ9lnhEyo,iJWjIWh6TkMZm1NY8qBranKTIuwsCPkVPG6T6c6ft-I,AQACAAE/Sone/0");
725                         /* Bombe’s Sone. */
726                         loadSone("USK@RuW~uAO35Ipne896-1OmaVJNPuYE4ZIB5oZ5ziaU57A,7rV3uiyztXBDt03DCoRiNwiGjgFCJuznM9Okc1opURU,AQACAAE/Sone/15");
727                         try {
728                                 configuration.getBooleanValue("FirstStart").setValue(false);
729                         } catch (ConfigurationException ce1) {
730                                 logger.log(Level.WARNING, "Could not clear “first start” flag!");
731                         }
732                 }
733
734                 options.addIntegerOption("InsertionDelay", new DefaultOption<Integer>(60, new OptionWatcher<Integer>() {
735
736                         @Override
737                         public void optionChanged(Option<Integer> option, Integer oldValue, Integer newValue) {
738                                 SoneInserter.setInsertionDelay(newValue);
739                         }
740
741                 }));
742
743                 options.addBooleanOption("ClearOnNextRestart", new DefaultOption<Boolean>(false));
744                 options.addBooleanOption("ReallyClearOnNextRestart", new DefaultOption<Boolean>(false));
745
746                 if (firstStart) {
747                         return;
748                 }
749
750                 options.getBooleanOption("ClearOnNextRestart").set(configuration.getBooleanValue("Option/ClearOnNextRestart").getValue(null));
751                 options.getBooleanOption("ReallyClearOnNextRestart").set(configuration.getBooleanValue("Option/ReallyClearOnNextRestart").getValue(null));
752                 boolean clearConfiguration = options.getBooleanOption("ClearOnNextRestart").get() && options.getBooleanOption("ReallyClearOnNextRestart").get();
753                 options.getBooleanOption("ClearOnNextRestart").set(null);
754                 options.getBooleanOption("ReallyClearOnNextRestart").set(null);
755                 if (clearConfiguration) {
756                         /* stop loading the configuration. */
757                         return;
758                 }
759
760                 options.getIntegerOption("InsertionDelay").set(configuration.getIntValue("Option/InsertionDelay").getValue(null));
761
762                 /* parse local Sones. */
763                 logger.log(Level.INFO, "Loading Sones…");
764                 int soneId = 0;
765                 do {
766                         String sonePrefix = "Sone/Sone." + soneId++;
767                         String id = configuration.getStringValue(sonePrefix + "/ID").getValue(null);
768                         if (id == null) {
769                                 break;
770                         }
771                         String name = configuration.getStringValue(sonePrefix + "/Name").getValue(null);
772                         long time = configuration.getLongValue(sonePrefix + "/Time").getValue((long) 0);
773                         String insertUri = configuration.getStringValue(sonePrefix + "/InsertURI").getValue(null);
774                         String requestUri = configuration.getStringValue(sonePrefix + "/RequestURI").getValue(null);
775                         long modificationCounter = configuration.getLongValue(sonePrefix + "/ModificationCounter").getValue((long) 0);
776                         String firstName = configuration.getStringValue(sonePrefix + "/Profile/FirstName").getValue(null);
777                         String middleName = configuration.getStringValue(sonePrefix + "/Profile/MiddleName").getValue(null);
778                         String lastName = configuration.getStringValue(sonePrefix + "/Profile/LastName").getValue(null);
779                         Integer birthDay = configuration.getIntValue(sonePrefix + "/Profile/BirthDay").getValue(null);
780                         Integer birthMonth = configuration.getIntValue(sonePrefix + "/Profile/BirthMonth").getValue(null);
781                         Integer birthYear = configuration.getIntValue(sonePrefix + "/Profile/BirthYear").getValue(null);
782                         try {
783                                 Profile profile = new Profile();
784                                 profile.setFirstName(firstName);
785                                 profile.setMiddleName(middleName);
786                                 profile.setLastName(lastName);
787                                 profile.setBirthDay(birthDay).setBirthMonth(birthMonth).setBirthYear(birthYear);
788                                 Sone sone = getSone(id).setName(name).setTime(time).setRequestUri(new FreenetURI(requestUri)).setInsertUri(new FreenetURI(insertUri));
789                                 sone.setProfile(profile);
790                                 int postId = 0;
791                                 do {
792                                         String postPrefix = sonePrefix + "/Post." + postId++;
793                                         id = configuration.getStringValue(postPrefix + "/ID").getValue(null);
794                                         if (id == null) {
795                                                 break;
796                                         }
797                                         time = configuration.getLongValue(postPrefix + "/Time").getValue((long) 0);
798                                         String text = configuration.getStringValue(postPrefix + "/Text").getValue(null);
799                                         Post post = getPost(id).setSone(sone).setTime(time).setText(text);
800                                         sone.addPost(post);
801                                 } while (true);
802                                 int replyCounter = 0;
803                                 do {
804                                         String replyPrefix = sonePrefix + "/Reply." + replyCounter++;
805                                         String replyId = configuration.getStringValue(replyPrefix + "/ID").getValue(null);
806                                         if (replyId == null) {
807                                                 break;
808                                         }
809                                         Post replyPost = getPost(configuration.getStringValue(replyPrefix + "/Post").getValue(null));
810                                         long replyTime = configuration.getLongValue(replyPrefix + "/Time").getValue(null);
811                                         String replyText = configuration.getStringValue(replyPrefix + "/Text").getValue(null);
812                                         Reply reply = getReply(replyId).setSone(sone).setPost(replyPost).setTime(replyTime).setText(replyText);
813                                         sone.addReply(reply);
814                                 } while (true);
815
816                                 /* load friends. */
817                                 int friendCounter = 0;
818                                 while (true) {
819                                         String friendPrefix = sonePrefix + "/Friend." + friendCounter++;
820                                         String friendId = configuration.getStringValue(friendPrefix + "/ID").getValue(null);
821                                         if (friendId == null) {
822                                                 break;
823                                         }
824                                         Sone friendSone = getSone(friendId);
825                                         String friendKey = configuration.getStringValue(friendPrefix + "/Key").getValue(null);
826                                         String friendName = configuration.getStringValue(friendPrefix + "/Name").getValue(null);
827                                         friendSone.setRequestUri(new FreenetURI(friendKey)).setName(friendName);
828                                         sone.addFriend(friendSone);
829                                 }
830
831                                 /* load blocked Sone IDs. */
832                                 int blockedSoneCounter = 0;
833                                 while (true) {
834                                         String blockedSonePrefix = sonePrefix + "/BlockedSone." + blockedSoneCounter++;
835                                         String blockedSoneId = configuration.getStringValue(blockedSonePrefix + "/ID").getValue(null);
836                                         if (blockedSoneId == null) {
837                                                 break;
838                                         }
839                                         sone.addBlockedSoneId(blockedSoneId);
840                                 }
841
842                                 /* load liked post IDs. */
843                                 int likedPostIdCounter = 0;
844                                 while (true) {
845                                         String likedPostIdPrefix = sonePrefix + "/LikedPostId." + likedPostIdCounter++;
846                                         String likedPostId = configuration.getStringValue(likedPostIdPrefix + "/ID").getValue(null);
847                                         if (likedPostId == null) {
848                                                 break;
849                                         }
850                                         sone.addLikedPostId(likedPostId);
851                                 }
852
853                                 /* load liked reply IDs. */
854                                 int likedReplyIdCounter = 0;
855                                 while (true) {
856                                         String likedReplyIdPrefix = sonePrefix + "/LikedReplyId." + likedReplyIdCounter++;
857                                         String likedReplyId = configuration.getStringValue(likedReplyIdPrefix + "/ID").getValue(null);
858                                         if (likedReplyId == null) {
859                                                 break;
860                                         }
861                                         sone.addLikedReplyId(likedReplyId);
862                                 }
863
864                                 sone.setModificationCounter(modificationCounter);
865                                 addLocalSone(sone);
866                         } catch (MalformedURLException mue1) {
867                                 logger.log(Level.WARNING, "Could not create Sone from requestUri (“" + requestUri + "”) and insertUri (“" + insertUri + "”)!", mue1);
868                         }
869                 } while (true);
870                 logger.log(Level.INFO, "Loaded %d Sones.", getSones().size());
871
872                 /* load all known Sones. */
873                 int knownSonesCounter = 0;
874                 while (true) {
875                         String knownSonePrefix = "KnownSone." + knownSonesCounter++;
876                         String knownSoneId = configuration.getStringValue(knownSonePrefix + "/ID").getValue(null);
877                         if (knownSoneId == null) {
878                                 break;
879                         }
880                         String knownSoneName = configuration.getStringValue(knownSonePrefix + "/Name").getValue(null);
881                         String knownSoneKey = configuration.getStringValue(knownSonePrefix + "/Key").getValue(null);
882                         try {
883                                 getSone(knownSoneId).setName(knownSoneName).setRequestUri(new FreenetURI(knownSoneKey));
884                         } catch (MalformedURLException mue1) {
885                                 logger.log(Level.WARNING, "Could not create Sone from requestUri (“" + knownSoneKey + "”)!", mue1);
886                         }
887                 }
888
889                 /* load all blacklisted Sones. */
890                 int blacklistedSonesCounter = 0;
891                 while (true) {
892                         String blacklistedSonePrefix = "BlacklistedSone." + blacklistedSonesCounter++;
893                         String blacklistedSoneId = configuration.getStringValue(blacklistedSonePrefix + "/ID").getValue(null);
894                         if (blacklistedSoneId == null) {
895                                 break;
896                         }
897                         String blacklistedSoneName = configuration.getStringValue(blacklistedSonePrefix + "/Name").getValue(null);
898                         String blacklistedSoneKey = configuration.getStringValue(blacklistedSonePrefix + "/Key").getValue(null);
899                         String blacklistedSoneInsertKey = configuration.getStringValue(blacklistedSonePrefix + "/InsertKey").getValue(null);
900                         try {
901                                 blacklistSone(getSone(blacklistedSoneId).setName(blacklistedSoneName).setRequestUri(new FreenetURI(blacklistedSoneKey)).setInsertUri((blacklistedSoneInsertKey != null) ? new FreenetURI(blacklistedSoneInsertKey) : null));
902                         } catch (MalformedURLException mue1) {
903                                 logger.log(Level.WARNING, "Could not create blacklisted Sone from requestUri (“" + blacklistedSoneKey + "”)!", mue1);
904                         }
905                 }
906
907                 /* load all remote Sones. */
908                 for (Sone remoteSone : getRemoteSones()) {
909                         loadSone(remoteSone);
910                 }
911
912                 logger.exiting(Core.class.getName(), "loadConfiguration()");
913         }
914
915         /**
916          * Saves the configuraiton.
917          */
918         private void saveConfiguration() {
919                 Set<Sone> sones = getSones();
920                 logger.log(Level.INFO, "Storing %d Sones…", sones.size());
921
922                 try {
923                         /* store the options first. */
924                         configuration.getIntValue("Option/InsertionDelay").setValue(options.getIntegerOption("InsertionDelay").getReal());
925                         configuration.getBooleanValue("Option/ClearOnNextRestart").setValue(options.getBooleanOption("ClearOnNextRestart").getReal());
926                         configuration.getBooleanValue("Option/ReallyClearOnNextRestart").setValue(options.getBooleanOption("ReallyClearOnNextRestart").getReal());
927
928                         /* store all Sones. */
929                         int soneId = 0;
930                         for (Sone sone : localSones) {
931                                 String sonePrefix = "Sone/Sone." + soneId++;
932                                 configuration.getStringValue(sonePrefix + "/ID").setValue(sone.getId());
933                                 configuration.getStringValue(sonePrefix + "/Name").setValue(sone.getName());
934                                 configuration.getLongValue(sonePrefix + "/Time").setValue(sone.getTime());
935                                 configuration.getStringValue(sonePrefix + "/RequestURI").setValue(sone.getRequestUri().toString());
936                                 configuration.getStringValue(sonePrefix + "/InsertURI").setValue(sone.getInsertUri().toString());
937                                 configuration.getLongValue(sonePrefix + "/ModificationCounter").setValue(sone.getModificationCounter());
938                                 Profile profile = sone.getProfile();
939                                 configuration.getStringValue(sonePrefix + "/Profile/FirstName").setValue(profile.getFirstName());
940                                 configuration.getStringValue(sonePrefix + "/Profile/MiddleName").setValue(profile.getMiddleName());
941                                 configuration.getStringValue(sonePrefix + "/Profile/LastName").setValue(profile.getLastName());
942                                 configuration.getIntValue(sonePrefix + "/Profile/BirthDay").setValue(profile.getBirthDay());
943                                 configuration.getIntValue(sonePrefix + "/Profile/BirthMonth").setValue(profile.getBirthMonth());
944                                 configuration.getIntValue(sonePrefix + "/Profile/BirthYear").setValue(profile.getBirthYear());
945                                 int postId = 0;
946                                 for (Post post : sone.getPosts()) {
947                                         String postPrefix = sonePrefix + "/Post." + postId++;
948                                         configuration.getStringValue(postPrefix + "/ID").setValue(post.getId());
949                                         configuration.getLongValue(postPrefix + "/Time").setValue(post.getTime());
950                                         configuration.getStringValue(postPrefix + "/Text").setValue(post.getText());
951                                 }
952                                 /* write null ID as terminator. */
953                                 configuration.getStringValue(sonePrefix + "/Post." + postId + "/ID").setValue(null);
954
955                                 int replyId = 0;
956                                 for (Reply reply : sone.getReplies()) {
957                                         String replyPrefix = sonePrefix + "/Reply." + replyId++;
958                                         configuration.getStringValue(replyPrefix + "/ID").setValue(reply.getId());
959                                         configuration.getStringValue(replyPrefix + "/Post").setValue(reply.getPost().getId());
960                                         configuration.getLongValue(replyPrefix + "/Time").setValue(reply.getTime());
961                                         configuration.getStringValue(replyPrefix + "/Text").setValue(reply.getText());
962                                 }
963                                 /* write null ID as terminator. */
964                                 configuration.getStringValue(sonePrefix + "/Reply." + replyId + "/ID").setValue(null);
965
966                                 int friendId = 0;
967                                 for (Sone friend : sone.getFriends()) {
968                                         String friendPrefix = sonePrefix + "/Friend." + friendId++;
969                                         configuration.getStringValue(friendPrefix + "/ID").setValue(friend.getId());
970                                         configuration.getStringValue(friendPrefix + "/Key").setValue(friend.getRequestUri().toString());
971                                         configuration.getStringValue(friendPrefix + "/Name").setValue(friend.getName());
972                                 }
973                                 /* write null ID as terminator. */
974                                 configuration.getStringValue(sonePrefix + "/Friend." + friendId + "/ID").setValue(null);
975
976                                 /* write all blocked Sones. */
977                                 int blockedSoneCounter = 0;
978                                 for (String blockedSoneId : sone.getBlockedSoneIds()) {
979                                         String blockedSonePrefix = sonePrefix + "/BlockedSone." + blockedSoneCounter++;
980                                         configuration.getStringValue(blockedSonePrefix + "/ID").setValue(blockedSoneId);
981                                 }
982                                 configuration.getStringValue(sonePrefix + "/BlockedSone." + blockedSoneCounter + "/ID").setValue(null);
983
984                                 /* write all liked posts. */
985                                 int likedPostIdCounter = 0;
986                                 for (String soneLikedPostId : sone.getLikedPostIds()) {
987                                         String likedPostIdPrefix = sonePrefix + "/LikedPostId." + likedPostIdCounter++;
988                                         configuration.getStringValue(likedPostIdPrefix + "/ID").setValue(soneLikedPostId);
989                                 }
990                                 configuration.getStringValue(sonePrefix + "/LikedPostId." + likedPostIdCounter + "/ID").setValue(null);
991
992                                 /* write all liked replies. */
993                                 int likedReplyIdCounter = 0;
994                                 for (String soneLikedReplyId : sone.getLikedReplyIds()) {
995                                         String likedReplyIdPrefix = sonePrefix + "/LikedReplyId." + likedReplyIdCounter++;
996                                         configuration.getStringValue(likedReplyIdPrefix + "/ID").setValue(soneLikedReplyId);
997                                 }
998                                 configuration.getStringValue(sonePrefix + "/LikedReplyId." + likedReplyIdCounter + "/ID").setValue(null);
999
1000                         }
1001                         /* write null ID as terminator. */
1002                         configuration.getStringValue("Sone/Sone." + soneId + "/ID").setValue(null);
1003
1004                         /* write all known Sones. */
1005                         int knownSonesCounter = 0;
1006                         for (Sone knownSone : getRemoteSones()) {
1007                                 String knownSonePrefix = "KnownSone." + knownSonesCounter++;
1008                                 configuration.getStringValue(knownSonePrefix + "/ID").setValue(knownSone.getId());
1009                                 configuration.getStringValue(knownSonePrefix + "/Name").setValue(knownSone.getName());
1010                                 configuration.getStringValue(knownSonePrefix + "/Key").setValue(knownSone.getRequestUri().toString());
1011                                 /* TODO - store all known stuff? */
1012                         }
1013                         configuration.getStringValue("KnownSone." + knownSonesCounter + "/ID").setValue(null);
1014
1015                         /* write all blacklisted Sones. */
1016                         int blacklistedSonesCounter = 0;
1017                         for (Sone blacklistedSone : getBlacklistedSones()) {
1018                                 String blacklistedSonePrefix = "BlacklistedSone." + blacklistedSonesCounter++;
1019                                 configuration.getStringValue(blacklistedSonePrefix + "/ID").setValue(blacklistedSone.getId());
1020                                 configuration.getStringValue(blacklistedSonePrefix + "/Name").setValue(blacklistedSone.getName());
1021                                 configuration.getStringValue(blacklistedSonePrefix + "/Key").setValue(blacklistedSone.getRequestUri().toString());
1022                                 configuration.getStringValue(blacklistedSonePrefix + "/InsertKey").setValue((blacklistedSone.getInsertUri() != null) ? blacklistedSone.getInsertUri().toString() : null);
1023                                 /* TODO - store all known stuff? */
1024                         }
1025                         configuration.getStringValue("BlacklistedSone." + blacklistedSonesCounter + "/ID").setValue(null);
1026
1027                 } catch (ConfigurationException ce1) {
1028                         logger.log(Level.WARNING, "Could not store configuration!", ce1);
1029                 }
1030         }
1031
1032 }