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