Too many changes to list them all.
[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.Reply;
36 import net.pterodactylus.sone.data.Sone;
37 import net.pterodactylus.sone.freenet.wot.Identity;
38 import net.pterodactylus.sone.freenet.wot.IdentityListener;
39 import net.pterodactylus.sone.freenet.wot.IdentityManager;
40 import net.pterodactylus.sone.freenet.wot.OwnIdentity;
41 import net.pterodactylus.util.config.Configuration;
42 import net.pterodactylus.util.config.ConfigurationException;
43 import net.pterodactylus.util.logging.Logging;
44 import net.pterodactylus.util.number.Numbers;
45 import freenet.keys.FreenetURI;
46
47 /**
48  * The Sone core.
49  *
50  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
51  */
52 public class Core implements IdentityListener {
53
54         /**
55          * Enumeration for the possible states of a {@link Sone}.
56          *
57          * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
58          */
59         public enum SoneStatus {
60
61                 /** The Sone is unknown, i.e. not yet downloaded. */
62                 unknown,
63
64                 /** The Sone is idle, i.e. not being downloaded or inserted. */
65                 idle,
66
67                 /** The Sone is currently being inserted. */
68                 inserting,
69
70                 /** The Sone is currently being downloaded. */
71                 downloading,
72         }
73
74         /** The logger. */
75         private static final Logger logger = Logging.getLogger(Core.class);
76
77         /** The options. */
78         private final Options options = new Options();
79
80         /** The configuration. */
81         private final Configuration configuration;
82
83         /** The identity manager. */
84         private final IdentityManager identityManager;
85
86         /** Interface to freenet. */
87         private final FreenetInterface freenetInterface;
88
89         /** The Sone downloader. */
90         private final SoneDownloader soneDownloader;
91
92         /** The Sones’ statuses. */
93         /* synchronize access on itself. */
94         private final Map<Sone, SoneStatus> soneStatuses = new HashMap<Sone, SoneStatus>();
95
96         /** Sone inserters. */
97         /* synchronize access on this on localSones. */
98         private final Map<Sone, SoneInserter> soneInserters = new HashMap<Sone, SoneInserter>();
99
100         /** All local Sones. */
101         /* synchronize access on this on itself. */
102         private Map<String, Sone> localSones = new HashMap<String, Sone>();
103
104         /** All remote Sones. */
105         /* synchronize access on this on itself. */
106         private Map<String, Sone> remoteSones = new HashMap<String, Sone>();
107
108         /** All posts. */
109         private Map<String, Post> posts = new HashMap<String, Post>();
110
111         /** All replies. */
112         private Map<String, Reply> replies = new HashMap<String, Reply>();
113
114         /**
115          * Creates a new core.
116          *
117          * @param configuration
118          *            The configuration of the core
119          * @param freenetInterface
120          *            The freenet interface
121          * @param identityManager
122          *            The identity manager
123          */
124         public Core(Configuration configuration, FreenetInterface freenetInterface, IdentityManager identityManager) {
125                 this.configuration = configuration;
126                 this.freenetInterface = freenetInterface;
127                 this.identityManager = identityManager;
128                 this.soneDownloader = new SoneDownloader(this, freenetInterface);
129         }
130
131         //
132         // ACCESSORS
133         //
134
135         /**
136          * Returns the options used by the core.
137          *
138          * @return The options of the core
139          */
140         public Options getOptions() {
141                 return options;
142         }
143
144         /**
145          * Returns the identity manager used by the core.
146          *
147          * @return The identity manager
148          */
149         public IdentityManager getIdentityManager() {
150                 return identityManager;
151         }
152
153         /**
154          * Returns the status of the given Sone.
155          *
156          * @param sone
157          *            The Sone to get the status for
158          * @return The status of the Sone
159          */
160         public SoneStatus getSoneStatus(Sone sone) {
161                 synchronized (soneStatuses) {
162                         return soneStatuses.get(sone);
163                 }
164         }
165
166         /**
167          * Sets the status of the given Sone.
168          *
169          * @param sone
170          *            The Sone to set the status of
171          * @param soneStatus
172          *            The status to set
173          */
174         public void setSoneStatus(Sone sone, SoneStatus soneStatus) {
175                 synchronized (soneStatuses) {
176                         soneStatuses.put(sone, soneStatus);
177                 }
178         }
179
180         /**
181          * Returns all Sones, remote and local.
182          *
183          * @return All Sones
184          */
185         public Set<Sone> getSones() {
186                 Set<Sone> allSones = new HashSet<Sone>();
187                 allSones.addAll(getLocalSones());
188                 allSones.addAll(getRemoteSones());
189                 return allSones;
190         }
191
192         /**
193          * Returns the Sone with the given ID, regardless whether it’s local or
194          * remote.
195          *
196          * @param id
197          *            The ID of the Sone to get
198          * @return The Sone with the given ID, or {@code null} if there is no such
199          *         Sone
200          */
201         public Sone getSone(String id) {
202                 Sone sone = getRemoteSone(id);
203                 if (sone != null) {
204                         return sone;
205                 }
206                 sone = getLocalSone(id);
207                 return sone;
208         }
209
210         /**
211          * Returns whether the given Sone is a local Sone.
212          *
213          * @param sone
214          *            The Sone to check for its locality
215          * @return {@code true} if the given Sone is local, {@code false} otherwise
216          */
217         public boolean isLocalSone(Sone sone) {
218                 synchronized (localSones) {
219                         return localSones.containsKey(sone.getId());
220                 }
221         }
222
223         /**
224          * Returns all local Sones.
225          *
226          * @return All local Sones
227          */
228         public Set<Sone> getLocalSones() {
229                 synchronized (localSones) {
230                         return new HashSet<Sone>(localSones.values());
231                 }
232         }
233
234         /**
235          * Returns the local Sone with the given ID.
236          *
237          * @param id
238          *            The ID of the Sone to get
239          * @return The Sone, or {@code null} if there is no Sone with the given ID
240          */
241         public Sone getLocalSone(String id) {
242                 synchronized (localSones) {
243                         return localSones.get(id);
244                 }
245         }
246
247         /**
248          * Returns all remote Sones.
249          *
250          * @return All remote Sones
251          */
252         public Set<Sone> getRemoteSones() {
253                 synchronized (remoteSones) {
254                         return new HashSet<Sone>(remoteSones.values());
255                 }
256         }
257
258         /**
259          * Returns the remote Sone with the given ID.
260          *
261          * @param id
262          *            The ID of the remote Sone to get
263          * @return The Sone, or {@code null} if there is no such Sone
264          */
265         public Sone getRemoteSone(String id) {
266                 synchronized (remoteSones) {
267                         return remoteSones.get(id);
268                 }
269         }
270
271         /**
272          * Returns whether the given Sone is a remote Sone.
273          *
274          * @param sone
275          *            The Sone to check
276          * @return {@code true} if the given Sone is a remote Sone, {@code false}
277          *         otherwise
278          */
279         public boolean isRemoteSone(Sone sone) {
280                 synchronized (remoteSones) {
281                         return remoteSones.containsKey(sone.getId());
282                 }
283         }
284
285         /**
286          * Returns the post with the given ID.
287          *
288          * @param postId
289          *            The ID of the post to get
290          * @return The post, or {@code null} if there is no such post
291          */
292         public Post getPost(String postId) {
293                 synchronized (posts) {
294                         return posts.get(postId);
295                 }
296         }
297
298         /**
299          * Returns the reply with the given ID.
300          *
301          * @param replyId
302          *            The ID of the reply to get
303          * @return The reply, or {@code null} if there is no such reply
304          */
305         public Reply getReply(String replyId) {
306                 synchronized (replies) {
307                         return replies.get(replyId);
308                 }
309         }
310
311         /**
312          * Returns all replies for the given post, order ascending by time.
313          *
314          * @param post
315          *            The post to get all replies for
316          * @return All replies for the given post
317          */
318         public List<Reply> getReplies(Post post) {
319                 Set<Sone> sones = getSones();
320                 List<Reply> replies = new ArrayList<Reply>();
321                 for (Sone sone : sones) {
322                         for (Reply reply : sone.getReplies()) {
323                                 if (reply.getPost().equals(post)) {
324                                         replies.add(reply);
325                                 }
326                         }
327                 }
328                 Collections.sort(replies, Reply.TIME_COMPARATOR);
329                 return replies;
330         }
331
332         /**
333          * Returns all Sones that have liked the given post.
334          *
335          * @param post
336          *            The post to get the liking Sones for
337          * @return The Sones that like the given post
338          */
339         public Set<Sone> getLikes(Post post) {
340                 Set<Sone> sones = new HashSet<Sone>();
341                 for (Sone sone : getSones()) {
342                         if (sone.getLikedPostIds().contains(post.getId())) {
343                                 sones.add(sone);
344                         }
345                 }
346                 return sones;
347         }
348
349         /**
350          * Returns all Sones that have liked the given reply.
351          *
352          * @param reply
353          *            The reply to get the liking Sones for
354          * @return The Sones that like the given reply
355          */
356         public Set<Sone> getLikes(Reply reply) {
357                 Set<Sone> sones = new HashSet<Sone>();
358                 for (Sone sone : getSones()) {
359                         if (sone.getLikedPostIds().contains(reply.getId())) {
360                                 sones.add(sone);
361                         }
362                 }
363                 return sones;
364         }
365
366         //
367         // ACTIONS
368         //
369
370         /**
371          * Adds a local Sone from the given ID which has to be the ID of an own
372          * identity.
373          *
374          * @param id
375          *            The ID of an own identity to add a Sone for
376          * @return The added (or already existing) Sone
377          */
378         public Sone addLocalSone(String id) {
379                 synchronized (localSones) {
380                         if (localSones.containsKey(id)) {
381                                 logger.log(Level.FINE, "Tried to add known local Sone: %s", id);
382                                 return localSones.get(id);
383                         }
384                         OwnIdentity ownIdentity = identityManager.getOwnIdentity(id);
385                         if (ownIdentity == null) {
386                                 logger.log(Level.INFO, "Invalid Sone ID: %s", id);
387                                 return null;
388                         }
389                         return addLocalSone(ownIdentity);
390                 }
391         }
392
393         /**
394          * Adds a local Sone from the given own identity.
395          *
396          * @param ownIdentity
397          *            The own identity to create a Sone from
398          * @return The added (or already existing) Sone
399          */
400         public Sone addLocalSone(OwnIdentity ownIdentity) {
401                 if (ownIdentity == null) {
402                         logger.log(Level.WARNING, "Given OwnIdentity is null!");
403                         return null;
404                 }
405                 synchronized (localSones) {
406                         if (localSones.containsKey(ownIdentity.getId())) {
407                                 logger.log(Level.FINE, "Tried to add known local Sone: %s", ownIdentity);
408                                 return localSones.get(ownIdentity.getId());
409                         }
410                         String latestEdition = ownIdentity.getProperty("Sone.LatestEdition");
411                         Sone sone = new Sone(ownIdentity).setInsertUri(getSoneUri(ownIdentity.getInsertUri(), latestEdition)).setRequestUri(getSoneUri(ownIdentity.getRequestUri(), latestEdition));
412                         /* TODO - load posts ’n stuff */
413                         localSones.put(ownIdentity.getId(), sone);
414                         SoneInserter soneInserter = new SoneInserter(this, freenetInterface, sone);
415                         soneInserters.put(sone, soneInserter);
416                         soneInserter.start();
417                         setSoneStatus(sone, SoneStatus.idle);
418                         return sone;
419                 }
420         }
421
422         /**
423          * Creates a new Sone for the given own identity.
424          *
425          * @param ownIdentity
426          *            The own identity to create a Sone for
427          * @return The created Sone
428          */
429         public Sone createSone(OwnIdentity ownIdentity) {
430                 identityManager.addContext(ownIdentity, "Sone");
431                 Sone sone = addLocalSone(ownIdentity);
432                 synchronized (sone) {
433                         /* mark as modified so that it gets inserted immediately. */
434                         sone.setModificationCounter(sone.getModificationCounter() + 1);
435                 }
436                 return sone;
437         }
438
439         /**
440          * Adds the Sone of the given identity.
441          *
442          * @param identity
443          *            The identity whose Sone to add
444          * @return The added or already existing Sone
445          */
446         public Sone addRemoteSone(Identity identity) {
447                 if (identity == null) {
448                         logger.log(Level.WARNING, "Given Identity is null!");
449                         return null;
450                 }
451                 synchronized (remoteSones) {
452                         if (remoteSones.containsKey(identity.getId())) {
453                                 logger.log(Level.FINE, "Identity already exists: %s", identity);
454                                 return remoteSones.get(identity.getId());
455                         }
456                         Sone sone = new Sone(identity);
457                         sone.setRequestUri(getSoneUri(identity.getRequestUri(), identity.getProperty("Sone.LatestEdition")));
458                         remoteSones.put(identity.getId(), sone);
459                         soneDownloader.addSone(sone);
460                         setSoneStatus(sone, SoneStatus.idle);
461                         return sone;
462                 }
463         }
464
465         /**
466          * Updates the stores Sone with the given Sone.
467          *
468          * @param sone
469          *            The updated Sone
470          */
471         public void updateSone(Sone sone) {
472                 if (isRemoteSone(sone)) {
473                         Sone storedSone = getRemoteSone(sone.getId());
474                         if (!(sone.getTime() > storedSone.getTime())) {
475                                 logger.log(Level.FINE, "Downloaded Sone %s is not newer than stored Sone %s.", new Object[] { sone, storedSone });
476                                 return;
477                         }
478                         synchronized (posts) {
479                                 for (Post post : storedSone.getPosts()) {
480                                         posts.remove(post.getId());
481                                 }
482                                 for (Post post : sone.getPosts()) {
483                                         posts.put(post.getId(), post);
484                                 }
485                         }
486                         synchronized (replies) {
487                                 for (Reply reply : storedSone.getReplies()) {
488                                         replies.remove(reply.getId());
489                                 }
490                                 for (Reply reply : sone.getReplies()) {
491                                         replies.put(reply.getId(), reply);
492                                 }
493                         }
494                         synchronized (storedSone) {
495                                 storedSone.setTime(sone.getTime());
496                                 storedSone.setProfile(sone.getProfile());
497                                 storedSone.setPosts(sone.getPosts());
498                                 storedSone.setReplies(sone.getReplies());
499                                 storedSone.setLikePostIds(sone.getLikedPostIds());
500                                 storedSone.setLikeReplyIds(sone.getLikedReplyIds());
501                                 storedSone.updateUris(sone.getRequestUri().getEdition());
502                         }
503                         saveSone(storedSone);
504                 }
505         }
506
507         /**
508          * Deletes the given Sone. This will remove the Sone from the
509          * {@link #getLocalSone(String) local Sones}, stops its {@link SoneInserter}
510          * and remove the context from its identity.
511          *
512          * @param sone
513          *            The Sone to delete
514          */
515         public void deleteSone(Sone sone) {
516                 if (!(sone.getIdentity() instanceof OwnIdentity)) {
517                         logger.log(Level.WARNING, "Tried to delete Sone of non-own identity: %s", sone);
518                         return;
519                 }
520                 synchronized (localSones) {
521                         if (!localSones.containsKey(sone.getId())) {
522                                 logger.log(Level.WARNING, "Tried to delete non-local Sone: %s", sone);
523                                 return;
524                         }
525                         localSones.remove(sone.getId());
526                         soneInserters.remove(sone.getId()).stop();
527                 }
528                 identityManager.removeContext((OwnIdentity) sone.getIdentity(), "Sone");
529         }
530
531         /**
532          * Saves the given Sone. This will persist all local settings for the given
533          * Sone, such as the friends list and similar, private options.
534          *
535          * @param sone
536          *            The Sone to save
537          */
538         public void saveSone(Sone sone) {
539                 if (!isLocalSone(sone)) {
540                         logger.log(Level.FINE, "Tried to save non-local Sone: %s", sone);
541                 }
542                 /* TODO - implement saving. */
543         }
544
545         /**
546          * Creates a new post.
547          *
548          * @param sone
549          *            The Sone that creates the post
550          * @param text
551          *            The text of the post
552          */
553         public void createPost(Sone sone, String text) {
554                 createPost(sone, System.currentTimeMillis(), text);
555         }
556
557         /**
558          * Creates a new post.
559          *
560          * @param sone
561          *            The Sone that creates the post
562          * @param time
563          *            The time of the post
564          * @param text
565          *            The text of the post
566          */
567         public void createPost(Sone sone, long time, String text) {
568                 if (!isLocalSone(sone)) {
569                         logger.log(Level.FINE, "Tried to create post for non-local Sone: %s", sone);
570                         return;
571                 }
572                 Post post = new Post(sone, time, text);
573                 synchronized (posts) {
574                         posts.put(post.getId(), post);
575                 }
576                 sone.addPost(post);
577                 saveSone(sone);
578         }
579
580         /**
581          * Deletes the given post.
582          *
583          * @param post
584          *            The post to delete
585          */
586         public void deletePost(Post post) {
587                 if (!isLocalSone(post.getSone())) {
588                         logger.log(Level.WARNING, "Tried to delete post of non-local Sone: %s", post.getSone());
589                         return;
590                 }
591                 post.getSone().removePost(post);
592                 synchronized (posts) {
593                         posts.remove(post.getId());
594                 }
595                 saveSone(post.getSone());
596         }
597
598         /**
599          * Creates a new reply.
600          *
601          * @param sone
602          *            The Sone that creates the reply
603          * @param post
604          *            The post that this reply refers to
605          * @param text
606          *            The text of the reply
607          */
608         public void createReply(Sone sone, Post post, String text) {
609                 createReply(sone, post, System.currentTimeMillis(), text);
610         }
611
612         /**
613          * Creates a new reply.
614          *
615          * @param sone
616          *            The Sone that creates the reply
617          * @param post
618          *            The post that this reply refers to
619          * @param time
620          *            The time of the reply
621          * @param text
622          *            The text of the reply
623          */
624         public void createReply(Sone sone, Post post, long time, String text) {
625                 if (!isLocalSone(sone)) {
626                         logger.log(Level.FINE, "Tried to create reply for non-local Sone: %s", sone);
627                         return;
628                 }
629                 Reply reply = new Reply(sone, post, System.currentTimeMillis(), text);
630                 synchronized (replies) {
631                         replies.put(reply.getId(), reply);
632                 }
633                 sone.addReply(reply);
634                 saveSone(sone);
635         }
636
637         /**
638          * Deletes the given reply.
639          *
640          * @param reply
641          *            The reply to delete
642          */
643         public void deleteReply(Reply reply) {
644                 Sone sone = reply.getSone();
645                 if (!isLocalSone(sone)) {
646                         logger.log(Level.FINE, "Tried to delete non-local reply: %s", reply);
647                         return;
648                 }
649                 synchronized (replies) {
650                         replies.remove(reply.getId());
651                 }
652                 sone.removeReply(reply);
653                 saveSone(sone);
654         }
655
656         /**
657          * Starts the core.
658          */
659         public void start() {
660                 loadConfiguration();
661         }
662
663         /**
664          * Stops the core.
665          */
666         public void stop() {
667                 synchronized (localSones) {
668                         for (SoneInserter soneInserter : soneInserters.values()) {
669                                 soneInserter.stop();
670                         }
671                 }
672                 saveConfiguration();
673         }
674
675         //
676         // PRIVATE METHODS
677         //
678
679         /**
680          * Loads the configuration.
681          */
682         @SuppressWarnings("unchecked")
683         private void loadConfiguration() {
684                 /* create options. */
685                 options.addIntegerOption("InsertionDelay", new DefaultOption<Integer>(60, new OptionWatcher<Integer>() {
686
687                         @Override
688                         public void optionChanged(Option<Integer> option, Integer oldValue, Integer newValue) {
689                                 SoneInserter.setInsertionDelay(newValue);
690                         }
691
692                 }));
693                 options.addBooleanOption("ClearOnNextRestart", new DefaultOption<Boolean>(false));
694                 options.addBooleanOption("ReallyClearOnNextRestart", new DefaultOption<Boolean>(false));
695
696                 /* read options from configuration. */
697                 options.getBooleanOption("ClearOnNextRestart").set(configuration.getBooleanValue("Option/ClearOnNextRestart").getValue(null));
698                 options.getBooleanOption("ReallyClearOnNextRestart").set(configuration.getBooleanValue("Option/ReallyClearOnNextRestart").getValue(null));
699                 boolean clearConfiguration = options.getBooleanOption("ClearOnNextRestart").get() && options.getBooleanOption("ReallyClearOnNextRestart").get();
700                 options.getBooleanOption("ClearOnNextRestart").set(null);
701                 options.getBooleanOption("ReallyClearOnNextRestart").set(null);
702                 if (clearConfiguration) {
703                         /* stop loading the configuration. */
704                         return;
705                 }
706
707                 options.getIntegerOption("InsertionDelay").set(configuration.getIntValue("Option/InsertionDelay").getValue(null));
708
709         }
710
711         /**
712          * Saves the current options.
713          */
714         private void saveConfiguration() {
715                 /* store the options first. */
716                 try {
717                         configuration.getIntValue("Option/InsertionDelay").setValue(options.getIntegerOption("InsertionDelay").getReal());
718                         configuration.getBooleanValue("Option/ClearOnNextRestart").setValue(options.getBooleanOption("ClearOnNextRestart").getReal());
719                         configuration.getBooleanValue("Option/ReallyClearOnNextRestart").setValue(options.getBooleanOption("ReallyClearOnNextRestart").getReal());
720                 } catch (ConfigurationException ce1) {
721                         logger.log(Level.SEVERE, "Could not store configuration!", ce1);
722                 }
723         }
724
725         /**
726          * Generate a Sone URI from the given URI and latest edition.
727          *
728          * @param uriString
729          *            The URI to derive the Sone URI from
730          * @param latestEditionString
731          *            The latest edition as a {@link String}, or {@code null}
732          * @return The derived URI
733          */
734         private FreenetURI getSoneUri(String uriString, String latestEditionString) {
735                 try {
736                         FreenetURI uri = new FreenetURI(uriString).setDocName("Sone").setMetaString(new String[0]).setSuggestedEdition(Numbers.safeParseLong(latestEditionString, (long) 0));
737                         return uri;
738                 } catch (MalformedURLException mue1) {
739                         logger.log(Level.WARNING, "Could not create Sone URI from URI: " + uriString, mue1);
740                         return null;
741                 }
742         }
743
744         //
745         // INTERFACE IdentityListener
746         //
747
748         /**
749          * {@inheritDoc}
750          */
751         @Override
752         public void ownIdentityAdded(OwnIdentity ownIdentity) {
753                 logger.log(Level.FINEST, "Adding OwnIdentity: " + ownIdentity);
754                 if (ownIdentity.hasContext("Sone")) {
755                         addLocalSone(ownIdentity);
756                 }
757         }
758
759         /**
760          * {@inheritDoc}
761          */
762         @Override
763         public void ownIdentityRemoved(OwnIdentity ownIdentity) {
764                 /* TODO */
765         }
766
767         /**
768          * {@inheritDoc}
769          */
770         @Override
771         public void identityAdded(Identity identity) {
772                 logger.log(Level.FINEST, "Adding Identity: " + identity);
773                 addRemoteSone(identity);
774         }
775
776         /**
777          * {@inheritDoc}
778          */
779         @Override
780         public void identityUpdated(Identity identity) {
781                 /* TODO */
782         }
783
784         /**
785          * {@inheritDoc}
786          */
787         @Override
788         public void identityRemoved(Identity identity) {
789                 /* TODO */
790         }
791
792 }