1bf66479822563deeb3708c5a55138ad1c24327d
[Sone.git] / src / main / java / net / pterodactylus / sone / core / Core.java
1 /*
2  * Sone - 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.net.MalformedURLException;
21 import java.util.ArrayList;
22 import java.util.Collections;
23 import java.util.HashMap;
24 import java.util.HashSet;
25 import java.util.List;
26 import java.util.Map;
27 import java.util.Set;
28 import java.util.logging.Level;
29 import java.util.logging.Logger;
30
31 import net.pterodactylus.sone.core.Options.DefaultOption;
32 import net.pterodactylus.sone.core.Options.Option;
33 import net.pterodactylus.sone.core.Options.OptionWatcher;
34 import net.pterodactylus.sone.data.Client;
35 import net.pterodactylus.sone.data.Post;
36 import net.pterodactylus.sone.data.Profile;
37 import net.pterodactylus.sone.data.Reply;
38 import net.pterodactylus.sone.data.Sone;
39 import net.pterodactylus.sone.freenet.wot.Identity;
40 import net.pterodactylus.sone.freenet.wot.IdentityListener;
41 import net.pterodactylus.sone.freenet.wot.IdentityManager;
42 import net.pterodactylus.sone.freenet.wot.OwnIdentity;
43 import net.pterodactylus.sone.main.SonePlugin;
44 import net.pterodactylus.util.config.Configuration;
45 import net.pterodactylus.util.config.ConfigurationException;
46 import net.pterodactylus.util.logging.Logging;
47 import net.pterodactylus.util.number.Numbers;
48 import freenet.keys.FreenetURI;
49
50 /**
51  * The Sone core.
52  *
53  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
54  */
55 public class Core implements IdentityListener {
56
57         /**
58          * Enumeration for the possible states of a {@link Sone}.
59          *
60          * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
61          */
62         public enum SoneStatus {
63
64                 /** The Sone is unknown, i.e. not yet downloaded. */
65                 unknown,
66
67                 /** The Sone is idle, i.e. not being downloaded or inserted. */
68                 idle,
69
70                 /** The Sone is currently being inserted. */
71                 inserting,
72
73                 /** The Sone is currently being downloaded. */
74                 downloading,
75         }
76
77         /** The logger. */
78         private static final Logger logger = Logging.getLogger(Core.class);
79
80         /** The options. */
81         private final Options options = new Options();
82
83         /** The core listener manager. */
84         private final CoreListenerManager coreListenerManager = new CoreListenerManager(this);
85
86         /** The configuration. */
87         private final Configuration configuration;
88
89         /** The identity manager. */
90         private final IdentityManager identityManager;
91
92         /** Interface to freenet. */
93         private final FreenetInterface freenetInterface;
94
95         /** The Sone downloader. */
96         private final SoneDownloader soneDownloader;
97
98         /** Whether the core has been stopped. */
99         private volatile boolean stopped;
100
101         /** The Sones’ statuses. */
102         /* synchronize access on itself. */
103         private final Map<Sone, SoneStatus> soneStatuses = new HashMap<Sone, SoneStatus>();
104
105         /** Sone inserters. */
106         /* synchronize access on this on localSones. */
107         private final Map<Sone, SoneInserter> soneInserters = new HashMap<Sone, SoneInserter>();
108
109         /** All local Sones. */
110         /* synchronize access on this on itself. */
111         private Map<String, Sone> localSones = new HashMap<String, Sone>();
112
113         /** All remote Sones. */
114         /* synchronize access on this on itself. */
115         private Map<String, Sone> remoteSones = new HashMap<String, Sone>();
116
117         /** All new Sones. */
118         private Set<String> newSones = new HashSet<String>();
119
120         /** All known Sones. */
121         /* synchronize access on {@link #newSones}. */
122         private Set<String> knownSones = new HashSet<String>();
123
124         /** All posts. */
125         private Map<String, Post> posts = new HashMap<String, Post>();
126
127         /** All new posts. */
128         private Set<String> newPosts = new HashSet<String>();
129
130         /** All known posts. */
131         /* synchronize access on {@link #newPosts}. */
132         private Set<String> knownPosts = new HashSet<String>();
133
134         /** All replies. */
135         private Map<String, Reply> replies = new HashMap<String, Reply>();
136
137         /** All new replies. */
138         private Set<String> newReplies = new HashSet<String>();
139
140         /** All known replies. */
141         private Set<String> knownReplies = new HashSet<String>();
142
143         /**
144          * Creates a new core.
145          *
146          * @param configuration
147          *            The configuration of the core
148          * @param freenetInterface
149          *            The freenet interface
150          * @param identityManager
151          *            The identity manager
152          */
153         public Core(Configuration configuration, FreenetInterface freenetInterface, IdentityManager identityManager) {
154                 this.configuration = configuration;
155                 this.freenetInterface = freenetInterface;
156                 this.identityManager = identityManager;
157                 this.soneDownloader = new SoneDownloader(this, freenetInterface);
158         }
159
160         //
161         // LISTENER MANAGEMENT
162         //
163
164         /**
165          * Adds a new core listener.
166          *
167          * @param coreListener
168          *            The listener to add
169          */
170         public void addCoreListener(CoreListener coreListener) {
171                 coreListenerManager.addListener(coreListener);
172         }
173
174         /**
175          * Removes a core listener.
176          *
177          * @param coreListener
178          *            The listener to remove
179          */
180         public void removeCoreListener(CoreListener coreListener) {
181                 coreListenerManager.removeListener(coreListener);
182         }
183
184         //
185         // ACCESSORS
186         //
187
188         /**
189          * Returns the options used by the core.
190          *
191          * @return The options of the core
192          */
193         public Options getOptions() {
194                 return options;
195         }
196
197         /**
198          * Returns the identity manager used by the core.
199          *
200          * @return The identity manager
201          */
202         public IdentityManager getIdentityManager() {
203                 return identityManager;
204         }
205
206         /**
207          * Returns the status of the given Sone.
208          *
209          * @param sone
210          *            The Sone to get the status for
211          * @return The status of the Sone
212          */
213         public SoneStatus getSoneStatus(Sone sone) {
214                 synchronized (soneStatuses) {
215                         return soneStatuses.get(sone);
216                 }
217         }
218
219         /**
220          * Sets the status of the given Sone.
221          *
222          * @param sone
223          *            The Sone to set the status of
224          * @param soneStatus
225          *            The status to set
226          */
227         public void setSoneStatus(Sone sone, SoneStatus soneStatus) {
228                 synchronized (soneStatuses) {
229                         soneStatuses.put(sone, soneStatus);
230                 }
231         }
232
233         /**
234          * Returns all Sones, remote and local.
235          *
236          * @return All Sones
237          */
238         public Set<Sone> getSones() {
239                 Set<Sone> allSones = new HashSet<Sone>();
240                 allSones.addAll(getLocalSones());
241                 allSones.addAll(getRemoteSones());
242                 return allSones;
243         }
244
245         /**
246          * Returns the Sone with the given ID, regardless whether it’s local or
247          * remote.
248          *
249          * @param id
250          *            The ID of the Sone to get
251          * @return The Sone with the given ID, or {@code null} if there is no such
252          *         Sone
253          */
254         public Sone getSone(String id) {
255                 return getSone(id, true);
256         }
257
258         /**
259          * Returns the Sone with the given ID, regardless whether it’s local or
260          * remote.
261          *
262          * @param id
263          *            The ID of the Sone to get
264          * @param create
265          *            {@code true} to create a new Sone if none exists,
266          *            {@code false} to return {@code null} if a Sone with the given
267          *            ID does not exist
268          * @return The Sone with the given ID, or {@code null} if there is no such
269          *         Sone
270          */
271         public Sone getSone(String id, boolean create) {
272                 if (isLocalSone(id)) {
273                         return getLocalSone(id);
274                 }
275                 return getRemoteSone(id, create);
276         }
277
278         /**
279          * Checks whether the core knows a Sone with the given ID.
280          *
281          * @param id
282          *            The ID of the Sone
283          * @return {@code true} if there is a Sone with the given ID, {@code false}
284          *         otherwise
285          */
286         public boolean hasSone(String id) {
287                 return isLocalSone(id) || isRemoteSone(id);
288         }
289
290         /**
291          * Returns whether the given Sone is a local Sone.
292          *
293          * @param sone
294          *            The Sone to check for its locality
295          * @return {@code true} if the given Sone is local, {@code false} otherwise
296          */
297         public boolean isLocalSone(Sone sone) {
298                 synchronized (localSones) {
299                         return localSones.containsKey(sone.getId());
300                 }
301         }
302
303         /**
304          * Returns whether the given ID is the ID of a local Sone.
305          *
306          * @param id
307          *            The Sone ID to check for its locality
308          * @return {@code true} if the given ID is a local Sone, {@code false}
309          *         otherwise
310          */
311         public boolean isLocalSone(String id) {
312                 synchronized (localSones) {
313                         return localSones.containsKey(id);
314                 }
315         }
316
317         /**
318          * Returns all local Sones.
319          *
320          * @return All local Sones
321          */
322         public Set<Sone> getLocalSones() {
323                 synchronized (localSones) {
324                         return new HashSet<Sone>(localSones.values());
325                 }
326         }
327
328         /**
329          * Returns the local Sone with the given ID.
330          *
331          * @param id
332          *            The ID of the Sone to get
333          * @return The Sone with the given ID
334          */
335         public Sone getLocalSone(String id) {
336                 return getLocalSone(id, true);
337         }
338
339         /**
340          * Returns the local Sone with the given ID, optionally creating a new Sone.
341          *
342          * @param id
343          *            The ID of the Sone
344          * @param create
345          *            {@code true} to create a new Sone if none exists,
346          *            {@code false} to return null if none exists
347          * @return The Sone with the given ID, or {@code null}
348          */
349         public Sone getLocalSone(String id, boolean create) {
350                 synchronized (localSones) {
351                         Sone sone = localSones.get(id);
352                         if ((sone == null) && create) {
353                                 sone = new Sone(id);
354                                 localSones.put(id, sone);
355                         }
356                         return sone;
357                 }
358         }
359
360         /**
361          * Returns all remote Sones.
362          *
363          * @return All remote Sones
364          */
365         public Set<Sone> getRemoteSones() {
366                 synchronized (remoteSones) {
367                         return new HashSet<Sone>(remoteSones.values());
368                 }
369         }
370
371         /**
372          * Returns the remote Sone with the given ID.
373          *
374          * @param id
375          *            The ID of the remote Sone to get
376          * @return The Sone with the given ID
377          */
378         public Sone getRemoteSone(String id) {
379                 return getRemoteSone(id, true);
380         }
381
382         /**
383          * Returns the remote Sone with the given ID.
384          *
385          * @param id
386          *            The ID of the remote Sone to get
387          * @param create
388          *            {@code true} to always create a Sone, {@code false} to return
389          *            {@code null} if no Sone with the given ID exists
390          * @return The Sone with the given ID
391          */
392         public Sone getRemoteSone(String id, boolean create) {
393                 synchronized (remoteSones) {
394                         Sone sone = remoteSones.get(id);
395                         if ((sone == null) && create) {
396                                 sone = new Sone(id);
397                                 remoteSones.put(id, sone);
398                         }
399                         return sone;
400                 }
401         }
402
403         /**
404          * Returns whether the given Sone is a remote Sone.
405          *
406          * @param sone
407          *            The Sone to check
408          * @return {@code true} if the given Sone is a remote Sone, {@code false}
409          *         otherwise
410          */
411         public boolean isRemoteSone(Sone sone) {
412                 synchronized (remoteSones) {
413                         return remoteSones.containsKey(sone.getId());
414                 }
415         }
416
417         /**
418          * Returns whether the Sone with the given ID is a remote Sone.
419          *
420          * @param id
421          *            The ID of the Sone to check
422          * @return {@code true} if the Sone with the given ID is a remote Sone,
423          *         {@code false} otherwise
424          */
425         public boolean isRemoteSone(String id) {
426                 synchronized (remoteSones) {
427                         return remoteSones.containsKey(id);
428                 }
429         }
430
431         /**
432          * Returns whether the given Sone is a new Sone. After this check, the Sone
433          * is marked as known, i.e. a second call with the same parameters will
434          * always yield {@code false}.
435          *
436          * @param sone
437          *            The sone to check for
438          * @return {@code true} if the given Sone is new, false otherwise
439          */
440         public boolean isNewSone(Sone sone) {
441                 synchronized (newSones) {
442                         boolean isNew = !knownSones.contains(sone.getId()) && newSones.remove(sone.getId());
443                         knownSones.add(sone.getId());
444                         if (isNew) {
445                                 coreListenerManager.fireMarkSoneKnown(sone);
446                         }
447                         return isNew;
448                 }
449         }
450
451         /**
452          * Returns whether the given Sone has been modified.
453          *
454          * @param sone
455          *            The Sone to check for modifications
456          * @return {@code true} if a modification has been detected in the Sone,
457          *         {@code false} otherwise
458          */
459         public boolean isModifiedSone(Sone sone) {
460                 return (soneInserters.containsKey(sone)) ? soneInserters.get(sone).isModified() : false;
461         }
462
463         /**
464          * Returns the post with the given ID.
465          *
466          * @param postId
467          *            The ID of the post to get
468          * @return The post, or {@code null} if there is no such post
469          */
470         public Post getPost(String postId) {
471                 synchronized (posts) {
472                         Post post = posts.get(postId);
473                         if (post == null) {
474                                 post = new Post(postId);
475                                 posts.put(postId, post);
476                         }
477                         return post;
478                 }
479         }
480
481         /**
482          * Returns whether the given post ID is new. After this method returns it is
483          * marked a known post ID.
484          *
485          * @param postId
486          *            The post ID
487          * @return {@code true} if the post is considered to be new, {@code false}
488          *         otherwise
489          */
490         public boolean isNewPost(String postId) {
491                 synchronized (newPosts) {
492                         boolean isNew = !knownPosts.contains(postId) && newPosts.remove(postId);
493                         knownPosts.add(postId);
494                         if (isNew) {
495                                 coreListenerManager.fireMarkPostKnown(getPost(postId));
496                         }
497                         return isNew;
498                 }
499         }
500
501         /**
502          * Returns the reply with the given ID.
503          *
504          * @param replyId
505          *            The ID of the reply to get
506          * @return The reply, or {@code null} if there is no such reply
507          */
508         public Reply getReply(String replyId) {
509                 synchronized (replies) {
510                         Reply reply = replies.get(replyId);
511                         if (reply == null) {
512                                 reply = new Reply(replyId);
513                                 replies.put(replyId, reply);
514                         }
515                         return reply;
516                 }
517         }
518
519         /**
520          * Returns all replies for the given post, order ascending by time.
521          *
522          * @param post
523          *            The post to get all replies for
524          * @return All replies for the given post
525          */
526         public List<Reply> getReplies(Post post) {
527                 Set<Sone> sones = getSones();
528                 List<Reply> replies = new ArrayList<Reply>();
529                 for (Sone sone : sones) {
530                         for (Reply reply : sone.getReplies()) {
531                                 if (reply.getPost().equals(post)) {
532                                         replies.add(reply);
533                                 }
534                         }
535                 }
536                 Collections.sort(replies, Reply.TIME_COMPARATOR);
537                 return replies;
538         }
539
540         /**
541          * Returns whether the reply with the given ID is new.
542          *
543          * @param replyId
544          *            The ID of the reply to check
545          * @return {@code true} if the reply is considered to be new, {@code false}
546          *         otherwise
547          */
548         public boolean isNewReply(String replyId) {
549                 synchronized (newReplies) {
550                         boolean isNew = !knownReplies.contains(replyId) && newReplies.remove(replyId);
551                         knownReplies.add(replyId);
552                         if (isNew) {
553                                 coreListenerManager.fireMarkReplyKnown(getReply(replyId));
554                         }
555                         return isNew;
556                 }
557         }
558
559         /**
560          * Returns all Sones that have liked the given post.
561          *
562          * @param post
563          *            The post to get the liking Sones for
564          * @return The Sones that like the given post
565          */
566         public Set<Sone> getLikes(Post post) {
567                 Set<Sone> sones = new HashSet<Sone>();
568                 for (Sone sone : getSones()) {
569                         if (sone.getLikedPostIds().contains(post.getId())) {
570                                 sones.add(sone);
571                         }
572                 }
573                 return sones;
574         }
575
576         /**
577          * Returns all Sones that have liked the given reply.
578          *
579          * @param reply
580          *            The reply to get the liking Sones for
581          * @return The Sones that like the given reply
582          */
583         public Set<Sone> getLikes(Reply reply) {
584                 Set<Sone> sones = new HashSet<Sone>();
585                 for (Sone sone : getSones()) {
586                         if (sone.getLikedReplyIds().contains(reply.getId())) {
587                                 sones.add(sone);
588                         }
589                 }
590                 return sones;
591         }
592
593         //
594         // ACTIONS
595         //
596
597         /**
598          * Adds a local Sone from the given ID which has to be the ID of an own
599          * identity.
600          *
601          * @param id
602          *            The ID of an own identity to add a Sone for
603          * @return The added (or already existing) Sone
604          */
605         public Sone addLocalSone(String id) {
606                 synchronized (localSones) {
607                         if (localSones.containsKey(id)) {
608                                 logger.log(Level.FINE, "Tried to add known local Sone: %s", id);
609                                 return localSones.get(id);
610                         }
611                         OwnIdentity ownIdentity = identityManager.getOwnIdentity(id);
612                         if (ownIdentity == null) {
613                                 logger.log(Level.INFO, "Invalid Sone ID: %s", id);
614                                 return null;
615                         }
616                         return addLocalSone(ownIdentity);
617                 }
618         }
619
620         /**
621          * Adds a local Sone from the given own identity.
622          *
623          * @param ownIdentity
624          *            The own identity to create a Sone from
625          * @return The added (or already existing) Sone
626          */
627         public Sone addLocalSone(OwnIdentity ownIdentity) {
628                 if (ownIdentity == null) {
629                         logger.log(Level.WARNING, "Given OwnIdentity is null!");
630                         return null;
631                 }
632                 synchronized (localSones) {
633                         final Sone sone;
634                         try {
635                                 sone = getLocalSone(ownIdentity.getId()).setIdentity(ownIdentity).setInsertUri(new FreenetURI(ownIdentity.getInsertUri())).setRequestUri(new FreenetURI(ownIdentity.getRequestUri()));
636                         } catch (MalformedURLException mue1) {
637                                 logger.log(Level.SEVERE, "Could not convert the Identity’s URIs to Freenet URIs: " + ownIdentity.getInsertUri() + ", " + ownIdentity.getRequestUri(), mue1);
638                                 return null;
639                         }
640                         sone.setLatestEdition(Numbers.safeParseLong(ownIdentity.getProperty("Sone.LatestEdition"), (long) 0));
641                         sone.setClient(new Client("Sone", SonePlugin.VERSION.toString()));
642                         /* TODO - load posts ’n stuff */
643                         localSones.put(ownIdentity.getId(), sone);
644                         SoneInserter soneInserter = new SoneInserter(this, freenetInterface, sone);
645                         soneInserters.put(sone, soneInserter);
646                         setSoneStatus(sone, SoneStatus.idle);
647                         loadSone(sone);
648                         soneInserter.start();
649                         new Thread(new Runnable() {
650
651                                 @Override
652                                 @SuppressWarnings("synthetic-access")
653                                 public void run() {
654                                         soneDownloader.fetchSone(sone);
655                                 }
656
657                         }, "Sone Downloader").start();
658                         return sone;
659                 }
660         }
661
662         /**
663          * Creates a new Sone for the given own identity.
664          *
665          * @param ownIdentity
666          *            The own identity to create a Sone for
667          * @return The created Sone
668          */
669         public Sone createSone(OwnIdentity ownIdentity) {
670                 identityManager.addContext(ownIdentity, "Sone");
671                 Sone sone = addLocalSone(ownIdentity);
672                 return sone;
673         }
674
675         /**
676          * Adds the Sone of the given identity.
677          *
678          * @param identity
679          *            The identity whose Sone to add
680          * @return The added or already existing Sone
681          */
682         public Sone addRemoteSone(Identity identity) {
683                 if (identity == null) {
684                         logger.log(Level.WARNING, "Given Identity is null!");
685                         return null;
686                 }
687                 synchronized (remoteSones) {
688                         final Sone sone = getRemoteSone(identity.getId()).setIdentity(identity);
689                         boolean newSone = sone.getRequestUri() == null;
690                         sone.setRequestUri(getSoneUri(identity.getRequestUri()));
691                         sone.setLatestEdition(Numbers.safeParseLong(identity.getProperty("Sone.LatestEdition"), (long) 0));
692                         if (newSone) {
693                                 synchronized (newSones) {
694                                         newSone = !knownSones.contains(sone.getId());
695                                         if (newSone) {
696                                                 newSones.add(sone.getId());
697                                         }
698                                 }
699                                 if (newSone) {
700                                         coreListenerManager.fireNewSoneFound(sone);
701                                 }
702                         }
703                         remoteSones.put(identity.getId(), sone);
704                         soneDownloader.addSone(sone);
705                         setSoneStatus(sone, SoneStatus.unknown);
706                         new Thread(new Runnable() {
707
708                                 @Override
709                                 @SuppressWarnings("synthetic-access")
710                                 public void run() {
711                                         soneDownloader.fetchSone(sone);
712                                 }
713
714                         }, "Sone Downloader").start();
715                         return sone;
716                 }
717         }
718
719         /**
720          * Updates the stores Sone with the given Sone.
721          *
722          * @param sone
723          *            The updated Sone
724          */
725         public void updateSone(Sone sone) {
726                 if (hasSone(sone.getId())) {
727                         Sone storedSone = getSone(sone.getId());
728                         if (!(sone.getTime() > storedSone.getTime())) {
729                                 logger.log(Level.FINE, "Downloaded Sone %s is not newer than stored Sone %s.", new Object[] { sone, storedSone });
730                                 return;
731                         }
732                         synchronized (posts) {
733                                 for (Post post : storedSone.getPosts()) {
734                                         posts.remove(post.getId());
735                                 }
736                                 synchronized (newPosts) {
737                                         for (Post post : sone.getPosts()) {
738                                                 if (!storedSone.getPosts().contains(post) && !knownPosts.contains(post.getId())) {
739                                                         newPosts.add(post.getId());
740                                                         coreListenerManager.fireNewPostFound(post);
741                                                 }
742                                                 posts.put(post.getId(), post);
743                                         }
744                                 }
745                         }
746                         synchronized (replies) {
747                                 for (Reply reply : storedSone.getReplies()) {
748                                         replies.remove(reply.getId());
749                                 }
750                                 synchronized (newReplies) {
751                                         for (Reply reply : sone.getReplies()) {
752                                                 if (!storedSone.getReplies().contains(reply) && !knownReplies.contains(reply.getId())) {
753                                                         newReplies.add(reply.getId());
754                                                         coreListenerManager.fireNewReplyFound(reply);
755                                                 }
756                                                 replies.put(reply.getId(), reply);
757                                         }
758                                 }
759                         }
760                         synchronized (storedSone) {
761                                 storedSone.setTime(sone.getTime());
762                                 storedSone.setClient(sone.getClient());
763                                 storedSone.setProfile(sone.getProfile());
764                                 storedSone.setPosts(sone.getPosts());
765                                 storedSone.setReplies(sone.getReplies());
766                                 storedSone.setLikePostIds(sone.getLikedPostIds());
767                                 storedSone.setLikeReplyIds(sone.getLikedReplyIds());
768                                 storedSone.setLatestEdition(sone.getRequestUri().getEdition());
769                         }
770                 }
771         }
772
773         /**
774          * Deletes the given Sone. This will remove the Sone from the
775          * {@link #getLocalSone(String) local Sones}, stops its {@link SoneInserter}
776          * and remove the context from its identity.
777          *
778          * @param sone
779          *            The Sone to delete
780          */
781         public void deleteSone(Sone sone) {
782                 if (!(sone.getIdentity() instanceof OwnIdentity)) {
783                         logger.log(Level.WARNING, "Tried to delete Sone of non-own identity: %s", sone);
784                         return;
785                 }
786                 synchronized (localSones) {
787                         if (!localSones.containsKey(sone.getId())) {
788                                 logger.log(Level.WARNING, "Tried to delete non-local Sone: %s", sone);
789                                 return;
790                         }
791                         localSones.remove(sone.getId());
792                         soneInserters.remove(sone).stop();
793                 }
794                 identityManager.removeContext((OwnIdentity) sone.getIdentity(), "Sone");
795                 identityManager.removeProperty((OwnIdentity) sone.getIdentity(), "Sone.LatestEdition");
796                 try {
797                         configuration.getLongValue("Sone/" + sone.getId() + "/Time").setValue(null);
798                 } catch (ConfigurationException ce1) {
799                         logger.log(Level.WARNING, "Could not remove Sone from configuration!", ce1);
800                 }
801         }
802
803         /**
804          * Loads and updates the given Sone from the configuration. If any error is
805          * encountered, loading is aborted and the given Sone is not changed.
806          *
807          * @param sone
808          *            The Sone to load and update
809          */
810         public void loadSone(Sone sone) {
811                 if (!isLocalSone(sone)) {
812                         logger.log(Level.FINE, "Tried to load non-local Sone: %s", sone);
813                         return;
814                 }
815
816                 /* load Sone. */
817                 String sonePrefix = "Sone/" + sone.getId();
818                 Long soneTime = configuration.getLongValue(sonePrefix + "/Time").getValue(null);
819                 if (soneTime == null) {
820                         logger.log(Level.INFO, "Could not load Sone because no Sone has been saved.");
821                         return;
822                 }
823                 String lastInsertFingerprint = configuration.getStringValue(sonePrefix + "/LastInsertFingerprint").getValue("");
824
825                 /* load profile. */
826                 Profile profile = new Profile();
827                 profile.setFirstName(configuration.getStringValue(sonePrefix + "/Profile/FirstName").getValue(null));
828                 profile.setMiddleName(configuration.getStringValue(sonePrefix + "/Profile/MiddleName").getValue(null));
829                 profile.setLastName(configuration.getStringValue(sonePrefix + "/Profile/LastName").getValue(null));
830                 profile.setBirthDay(configuration.getIntValue(sonePrefix + "/Profile/BirthDay").getValue(null));
831                 profile.setBirthMonth(configuration.getIntValue(sonePrefix + "/Profile/BirthMonth").getValue(null));
832                 profile.setBirthYear(configuration.getIntValue(sonePrefix + "/Profile/BirthYear").getValue(null));
833
834                 /* load posts. */
835                 Set<Post> posts = new HashSet<Post>();
836                 while (true) {
837                         String postPrefix = sonePrefix + "/Posts/" + posts.size();
838                         String postId = configuration.getStringValue(postPrefix + "/ID").getValue(null);
839                         if (postId == null) {
840                                 break;
841                         }
842                         long postTime = configuration.getLongValue(postPrefix + "/Time").getValue((long) 0);
843                         String postText = configuration.getStringValue(postPrefix + "/Text").getValue(null);
844                         if ((postTime == 0) || (postText == null)) {
845                                 logger.log(Level.WARNING, "Invalid post found, aborting load!");
846                                 return;
847                         }
848                         posts.add(getPost(postId).setSone(sone).setTime(postTime).setText(postText));
849                 }
850
851                 /* load replies. */
852                 Set<Reply> replies = new HashSet<Reply>();
853                 while (true) {
854                         String replyPrefix = sonePrefix + "/Replies/" + replies.size();
855                         String replyId = configuration.getStringValue(replyPrefix + "/ID").getValue(null);
856                         if (replyId == null) {
857                                 break;
858                         }
859                         String postId = configuration.getStringValue(replyPrefix + "/Post/ID").getValue(null);
860                         long replyTime = configuration.getLongValue(replyPrefix + "/Time").getValue((long) 0);
861                         String replyText = configuration.getStringValue(replyPrefix + "/Text").getValue(null);
862                         if ((postId == null) || (replyTime == 0) || (replyText == null)) {
863                                 logger.log(Level.WARNING, "Invalid reply found, aborting load!");
864                                 return;
865                         }
866                         replies.add(getReply(replyId).setSone(sone).setPost(getPost(postId)).setTime(replyTime).setText(replyText));
867                 }
868
869                 /* load post likes. */
870                 Set<String> likedPostIds = new HashSet<String>();
871                 while (true) {
872                         String likedPostId = configuration.getStringValue(sonePrefix + "/Likes/Post/" + likedPostIds.size() + "/ID").getValue(null);
873                         if (likedPostId == null) {
874                                 break;
875                         }
876                         likedPostIds.add(likedPostId);
877                 }
878
879                 /* load reply likes. */
880                 Set<String> likedReplyIds = new HashSet<String>();
881                 while (true) {
882                         String likedReplyId = configuration.getStringValue(sonePrefix + "/Likes/Reply/" + likedReplyIds.size() + "/ID").getValue(null);
883                         if (likedReplyId == null) {
884                                 break;
885                         }
886                         likedReplyIds.add(likedReplyId);
887                 }
888
889                 /* load friends. */
890                 Set<String> friends = new HashSet<String>();
891                 while (true) {
892                         String friendId = configuration.getStringValue(sonePrefix + "/Friends/" + friends.size() + "/ID").getValue(null);
893                         if (friendId == null) {
894                                 break;
895                         }
896                         friends.add(friendId);
897                 }
898
899                 /* if we’re still here, Sone was loaded successfully. */
900                 synchronized (sone) {
901                         sone.setTime(soneTime);
902                         sone.setProfile(profile);
903                         sone.setPosts(posts);
904                         sone.setReplies(replies);
905                         sone.setLikePostIds(likedPostIds);
906                         sone.setLikeReplyIds(likedReplyIds);
907                         sone.setFriends(friends);
908                         soneInserters.get(sone).setLastInsertFingerprint(lastInsertFingerprint);
909                 }
910                 synchronized (newSones) {
911                         for (String friend : friends) {
912                                 knownSones.add(friend);
913                         }
914                 }
915                 synchronized (newPosts) {
916                         for (Post post : posts) {
917                                 knownPosts.add(post.getId());
918                         }
919                 }
920                 synchronized (newReplies) {
921                         for (Reply reply : replies) {
922                                 knownReplies.add(reply.getId());
923                         }
924                 }
925         }
926
927         /**
928          * Saves the given Sone. This will persist all local settings for the given
929          * Sone, such as the friends list and similar, private options.
930          *
931          * @param sone
932          *            The Sone to save
933          */
934         public void saveSone(Sone sone) {
935                 if (!isLocalSone(sone)) {
936                         logger.log(Level.FINE, "Tried to save non-local Sone: %s", sone);
937                         return;
938                 }
939                 if (!(sone.getIdentity() instanceof OwnIdentity)) {
940                         logger.log(Level.WARNING, "Local Sone without OwnIdentity found, refusing to save: %s", sone);
941                         return;
942                 }
943
944                 logger.log(Level.INFO, "Saving Sone: %s", sone);
945                 identityManager.setProperty((OwnIdentity) sone.getIdentity(), "Sone.LatestEdition", String.valueOf(sone.getLatestEdition()));
946                 try {
947                         /* save Sone into configuration. */
948                         String sonePrefix = "Sone/" + sone.getId();
949                         configuration.getLongValue(sonePrefix + "/Time").setValue(sone.getTime());
950                         configuration.getStringValue(sonePrefix + "/LastInsertFingerprint").setValue(soneInserters.get(sone).getLastInsertFingerprint());
951
952                         /* save profile. */
953                         Profile profile = sone.getProfile();
954                         configuration.getStringValue(sonePrefix + "/Profile/FirstName").setValue(profile.getFirstName());
955                         configuration.getStringValue(sonePrefix + "/Profile/MiddleName").setValue(profile.getMiddleName());
956                         configuration.getStringValue(sonePrefix + "/Profile/LastName").setValue(profile.getLastName());
957                         configuration.getIntValue(sonePrefix + "/Profile/BirthDay").setValue(profile.getBirthDay());
958                         configuration.getIntValue(sonePrefix + "/Profile/BirthMonth").setValue(profile.getBirthMonth());
959                         configuration.getIntValue(sonePrefix + "/Profile/BirthYear").setValue(profile.getBirthYear());
960
961                         /* save posts. */
962                         int postCounter = 0;
963                         for (Post post : sone.getPosts()) {
964                                 String postPrefix = sonePrefix + "/Posts/" + postCounter++;
965                                 configuration.getStringValue(postPrefix + "/ID").setValue(post.getId());
966                                 configuration.getLongValue(postPrefix + "/Time").setValue(post.getTime());
967                                 configuration.getStringValue(postPrefix + "/Text").setValue(post.getText());
968                         }
969                         configuration.getStringValue(sonePrefix + "/Posts/" + postCounter + "/ID").setValue(null);
970
971                         /* save replies. */
972                         int replyCounter = 0;
973                         for (Reply reply : sone.getReplies()) {
974                                 String replyPrefix = sonePrefix + "/Replies/" + replyCounter++;
975                                 configuration.getStringValue(replyPrefix + "/ID").setValue(reply.getId());
976                                 configuration.getStringValue(replyPrefix + "/Post/ID").setValue(reply.getPost().getId());
977                                 configuration.getLongValue(replyPrefix + "/Time").setValue(reply.getTime());
978                                 configuration.getStringValue(replyPrefix + "/Text").setValue(reply.getText());
979                         }
980                         configuration.getStringValue(sonePrefix + "/Replies/" + replyCounter + "/ID").setValue(null);
981
982                         /* save post likes. */
983                         int postLikeCounter = 0;
984                         for (String postId : sone.getLikedPostIds()) {
985                                 configuration.getStringValue(sonePrefix + "/Likes/Post/" + postLikeCounter++ + "/ID").setValue(postId);
986                         }
987                         configuration.getStringValue(sonePrefix + "/Likes/Post/" + postLikeCounter + "/ID").setValue(null);
988
989                         /* save reply likes. */
990                         int replyLikeCounter = 0;
991                         for (String replyId : sone.getLikedReplyIds()) {
992                                 configuration.getStringValue(sonePrefix + "/Likes/Reply/" + replyLikeCounter++ + "/ID").setValue(replyId);
993                         }
994                         configuration.getStringValue(sonePrefix + "/Likes/Reply/" + replyLikeCounter + "/ID").setValue(null);
995
996                         /* save friends. */
997                         int friendCounter = 0;
998                         for (String friendId : sone.getFriends()) {
999                                 configuration.getStringValue(sonePrefix + "/Friends/" + friendCounter++ + "/ID").setValue(friendId);
1000                         }
1001                         configuration.getStringValue(sonePrefix + "/Friends/" + friendCounter + "/ID").setValue(null);
1002
1003                         logger.log(Level.INFO, "Sone %s saved.", sone);
1004                 } catch (ConfigurationException ce1) {
1005                         logger.log(Level.WARNING, "Could not save Sone: " + sone, ce1);
1006                 }
1007         }
1008
1009         /**
1010          * Creates a new post.
1011          *
1012          * @param sone
1013          *            The Sone that creates the post
1014          * @param text
1015          *            The text of the post
1016          */
1017         public void createPost(Sone sone, String text) {
1018                 createPost(sone, System.currentTimeMillis(), text);
1019         }
1020
1021         /**
1022          * Creates a new post.
1023          *
1024          * @param sone
1025          *            The Sone that creates the post
1026          * @param time
1027          *            The time of the post
1028          * @param text
1029          *            The text of the post
1030          */
1031         public void createPost(Sone sone, long time, String text) {
1032                 if (!isLocalSone(sone)) {
1033                         logger.log(Level.FINE, "Tried to create post for non-local Sone: %s", sone);
1034                         return;
1035                 }
1036                 Post post = new Post(sone, time, text);
1037                 synchronized (posts) {
1038                         posts.put(post.getId(), post);
1039                 }
1040                 synchronized (newPosts) {
1041                         knownPosts.add(post.getId());
1042                 }
1043                 sone.addPost(post);
1044                 saveSone(sone);
1045         }
1046
1047         /**
1048          * Deletes the given post.
1049          *
1050          * @param post
1051          *            The post to delete
1052          */
1053         public void deletePost(Post post) {
1054                 if (!isLocalSone(post.getSone())) {
1055                         logger.log(Level.WARNING, "Tried to delete post of non-local Sone: %s", post.getSone());
1056                         return;
1057                 }
1058                 post.getSone().removePost(post);
1059                 synchronized (posts) {
1060                         posts.remove(post.getId());
1061                 }
1062                 saveSone(post.getSone());
1063         }
1064
1065         /**
1066          * Creates a new reply.
1067          *
1068          * @param sone
1069          *            The Sone that creates the reply
1070          * @param post
1071          *            The post that this reply refers to
1072          * @param text
1073          *            The text of the reply
1074          * @return The created reply
1075          */
1076         public Reply createReply(Sone sone, Post post, String text) {
1077                 return createReply(sone, post, System.currentTimeMillis(), text);
1078         }
1079
1080         /**
1081          * Creates a new reply.
1082          *
1083          * @param sone
1084          *            The Sone that creates the reply
1085          * @param post
1086          *            The post that this reply refers to
1087          * @param time
1088          *            The time of the reply
1089          * @param text
1090          *            The text of the reply
1091          * @return The created reply
1092          */
1093         public Reply createReply(Sone sone, Post post, long time, String text) {
1094                 if (!isLocalSone(sone)) {
1095                         logger.log(Level.FINE, "Tried to create reply for non-local Sone: %s", sone);
1096                         return null;
1097                 }
1098                 Reply reply = new Reply(sone, post, System.currentTimeMillis(), text);
1099                 synchronized (replies) {
1100                         replies.put(reply.getId(), reply);
1101                 }
1102                 synchronized (newReplies) {
1103                         knownReplies.add(reply.getId());
1104                 }
1105                 sone.addReply(reply);
1106                 saveSone(sone);
1107                 return reply;
1108         }
1109
1110         /**
1111          * Deletes the given reply.
1112          *
1113          * @param reply
1114          *            The reply to delete
1115          */
1116         public void deleteReply(Reply reply) {
1117                 Sone sone = reply.getSone();
1118                 if (!isLocalSone(sone)) {
1119                         logger.log(Level.FINE, "Tried to delete non-local reply: %s", reply);
1120                         return;
1121                 }
1122                 synchronized (replies) {
1123                         replies.remove(reply.getId());
1124                 }
1125                 sone.removeReply(reply);
1126                 saveSone(sone);
1127         }
1128
1129         /**
1130          * Starts the core.
1131          */
1132         public void start() {
1133                 loadConfiguration();
1134         }
1135
1136         /**
1137          * Stops the core.
1138          */
1139         public void stop() {
1140                 synchronized (localSones) {
1141                         for (SoneInserter soneInserter : soneInserters.values()) {
1142                                 soneInserter.stop();
1143                         }
1144                 }
1145                 saveConfiguration();
1146                 stopped = true;
1147         }
1148
1149         //
1150         // PRIVATE METHODS
1151         //
1152
1153         /**
1154          * Loads the configuration.
1155          */
1156         @SuppressWarnings("unchecked")
1157         private void loadConfiguration() {
1158                 /* create options. */
1159                 options.addIntegerOption("InsertionDelay", new DefaultOption<Integer>(60, new OptionWatcher<Integer>() {
1160
1161                         @Override
1162                         public void optionChanged(Option<Integer> option, Integer oldValue, Integer newValue) {
1163                                 SoneInserter.setInsertionDelay(newValue);
1164                         }
1165
1166                 }));
1167                 options.addBooleanOption("ClearOnNextRestart", new DefaultOption<Boolean>(false));
1168                 options.addBooleanOption("ReallyClearOnNextRestart", new DefaultOption<Boolean>(false));
1169
1170                 /* read options from configuration. */
1171                 options.getBooleanOption("ClearOnNextRestart").set(configuration.getBooleanValue("Option/ClearOnNextRestart").getValue(null));
1172                 options.getBooleanOption("ReallyClearOnNextRestart").set(configuration.getBooleanValue("Option/ReallyClearOnNextRestart").getValue(null));
1173                 boolean clearConfiguration = options.getBooleanOption("ClearOnNextRestart").get() && options.getBooleanOption("ReallyClearOnNextRestart").get();
1174                 options.getBooleanOption("ClearOnNextRestart").set(null);
1175                 options.getBooleanOption("ReallyClearOnNextRestart").set(null);
1176                 if (clearConfiguration) {
1177                         /* stop loading the configuration. */
1178                         return;
1179                 }
1180
1181                 options.getIntegerOption("InsertionDelay").set(configuration.getIntValue("Option/InsertionDelay").getValue(null));
1182
1183                 /* load known Sones. */
1184                 int soneCounter = 0;
1185                 while (true) {
1186                         String knownSoneId = configuration.getStringValue("KnownSone/" + soneCounter++ + "/ID").getValue(null);
1187                         if (knownSoneId == null) {
1188                                 break;
1189                         }
1190                         synchronized (newSones) {
1191                                 knownSones.add(knownSoneId);
1192                         }
1193                 }
1194
1195                 /* load known posts. */
1196                 int postCounter = 0;
1197                 while (true) {
1198                         String knownPostId = configuration.getStringValue("KnownPosts/" + postCounter++ + "/ID").getValue(null);
1199                         if (knownPostId == null) {
1200                                 break;
1201                         }
1202                         synchronized (newPosts) {
1203                                 knownPosts.add(knownPostId);
1204                         }
1205                 }
1206
1207                 /* load known replies. */
1208                 int replyCounter = 0;
1209                 while (true) {
1210                         String knownReplyId = configuration.getStringValue("KnownReplies/" + replyCounter++ + "/ID").getValue(null);
1211                         if (knownReplyId == null) {
1212                                 break;
1213                         }
1214                         synchronized (newReplies) {
1215                                 knownReplies.add(knownReplyId);
1216                         }
1217                 }
1218
1219         }
1220
1221         /**
1222          * Saves the current options.
1223          */
1224         private void saveConfiguration() {
1225                 /* store the options first. */
1226                 try {
1227                         configuration.getIntValue("Option/InsertionDelay").setValue(options.getIntegerOption("InsertionDelay").getReal());
1228                         configuration.getBooleanValue("Option/ClearOnNextRestart").setValue(options.getBooleanOption("ClearOnNextRestart").getReal());
1229                         configuration.getBooleanValue("Option/ReallyClearOnNextRestart").setValue(options.getBooleanOption("ReallyClearOnNextRestart").getReal());
1230
1231                         /* save known Sones. */
1232                         int soneCounter = 0;
1233                         synchronized (newSones) {
1234                                 for (String knownSoneId : knownSones) {
1235                                         configuration.getStringValue("KnownSone/" + soneCounter++ + "/ID").setValue(knownSoneId);
1236                                 }
1237                                 configuration.getStringValue("KnownSone/" + soneCounter + "/ID").setValue(null);
1238                         }
1239
1240                         /* save known posts. */
1241                         int postCounter = 0;
1242                         synchronized (newPosts) {
1243                                 for (String knownPostId : knownPosts) {
1244                                         configuration.getStringValue("KnownPosts/" + postCounter++ + "/ID").setValue(knownPostId);
1245                                 }
1246                                 configuration.getStringValue("KnownPosts/" + postCounter + "/ID").setValue(null);
1247                         }
1248
1249                         /* save known replies. */
1250                         int replyCounter = 0;
1251                         synchronized (newReplies) {
1252                                 for (String knownReplyId : knownReplies) {
1253                                         configuration.getStringValue("KnownReplies/" + replyCounter++ + "/ID").setValue(knownReplyId);
1254                                 }
1255                                 configuration.getStringValue("KnownReplies/" + replyCounter + "/ID").setValue(null);
1256                         }
1257
1258                 } catch (ConfigurationException ce1) {
1259                         logger.log(Level.SEVERE, "Could not store configuration!", ce1);
1260                 }
1261         }
1262
1263         /**
1264          * Generate a Sone URI from the given URI and latest edition.
1265          *
1266          * @param uriString
1267          *            The URI to derive the Sone URI from
1268          * @return The derived URI
1269          */
1270         private FreenetURI getSoneUri(String uriString) {
1271                 try {
1272                         FreenetURI uri = new FreenetURI(uriString).setDocName("Sone").setMetaString(new String[0]);
1273                         return uri;
1274                 } catch (MalformedURLException mue1) {
1275                         logger.log(Level.WARNING, "Could not create Sone URI from URI: " + uriString, mue1);
1276                         return null;
1277                 }
1278         }
1279
1280         //
1281         // INTERFACE IdentityListener
1282         //
1283
1284         /**
1285          * {@inheritDoc}
1286          */
1287         @Override
1288         public void ownIdentityAdded(OwnIdentity ownIdentity) {
1289                 logger.log(Level.FINEST, "Adding OwnIdentity: " + ownIdentity);
1290                 if (ownIdentity.hasContext("Sone")) {
1291                         addLocalSone(ownIdentity);
1292                 }
1293         }
1294
1295         /**
1296          * {@inheritDoc}
1297          */
1298         @Override
1299         public void ownIdentityRemoved(OwnIdentity ownIdentity) {
1300                 logger.log(Level.FINEST, "Removing OwnIdentity: " + ownIdentity);
1301         }
1302
1303         /**
1304          * {@inheritDoc}
1305          */
1306         @Override
1307         public void identityAdded(Identity identity) {
1308                 logger.log(Level.FINEST, "Adding Identity: " + identity);
1309                 addRemoteSone(identity);
1310         }
1311
1312         /**
1313          * {@inheritDoc}
1314          */
1315         @Override
1316         public void identityUpdated(final Identity identity) {
1317                 new Thread(new Runnable() {
1318
1319                         @Override
1320                         @SuppressWarnings("synthetic-access")
1321                         public void run() {
1322                                 Sone sone = getRemoteSone(identity.getId());
1323                                 soneDownloader.fetchSone(sone);
1324                         }
1325                 }).start();
1326         }
1327
1328         /**
1329          * {@inheritDoc}
1330          */
1331         @Override
1332         public void identityRemoved(Identity identity) {
1333                 /* TODO */
1334         }
1335
1336 }