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