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