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