28d4521a8ca68c8f7c4406c5e330ecfa45bcf0e8
[Sone.git] / src / main / java / net / pterodactylus / sone / core / SoneDownloaderImpl.java
1 /*
2  * Sone - SoneDownloader.java - Copyright © 2010–2013 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 static java.lang.String.format;
21 import static java.lang.System.currentTimeMillis;
22 import static java.util.concurrent.TimeUnit.DAYS;
23
24 import java.io.InputStream;
25 import java.net.MalformedURLException;
26 import java.util.ArrayList;
27 import java.util.HashMap;
28 import java.util.HashSet;
29 import java.util.List;
30 import java.util.Map;
31 import java.util.Set;
32 import java.util.concurrent.TimeUnit;
33 import java.util.logging.Level;
34 import java.util.logging.Logger;
35
36 import net.pterodactylus.sone.core.FreenetInterface.Fetched;
37 import net.pterodactylus.sone.data.Album;
38 import net.pterodactylus.sone.data.Client;
39 import net.pterodactylus.sone.data.Image;
40 import net.pterodactylus.sone.data.Post;
41 import net.pterodactylus.sone.data.PostReply;
42 import net.pterodactylus.sone.data.Profile;
43 import net.pterodactylus.sone.data.Sone;
44 import net.pterodactylus.sone.data.Sone.SoneStatus;
45 import net.pterodactylus.sone.data.SoneImpl;
46 import net.pterodactylus.sone.database.PostBuilder;
47 import net.pterodactylus.sone.database.PostReplyBuilder;
48 import net.pterodactylus.sone.database.SoneBuilder;
49 import net.pterodactylus.util.io.Closer;
50 import net.pterodactylus.util.logging.Logging;
51 import net.pterodactylus.util.number.Numbers;
52 import net.pterodactylus.util.service.AbstractService;
53 import net.pterodactylus.util.xml.SimpleXML;
54 import net.pterodactylus.util.xml.XML;
55
56 import freenet.client.FetchResult;
57 import freenet.client.async.ClientContext;
58 import freenet.client.async.USKCallback;
59 import freenet.keys.FreenetURI;
60 import freenet.keys.USK;
61 import freenet.node.RequestStarter;
62 import freenet.support.api.Bucket;
63
64 import com.db4o.ObjectContainer;
65
66 import com.google.common.annotations.VisibleForTesting;
67 import org.w3c.dom.Document;
68
69 /**
70  * The Sone downloader is responsible for download Sones as they are updated.
71  *
72  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
73  */
74 public class SoneDownloaderImpl extends AbstractService implements SoneDownloader {
75
76         /** The logger. */
77         private static final Logger logger = Logging.getLogger(SoneDownloaderImpl.class);
78
79         /** The maximum protocol version. */
80         private static final int MAX_PROTOCOL_VERSION = 0;
81
82         /** The core. */
83         private final Core core;
84
85         /** The Freenet interface. */
86         private final FreenetInterface freenetInterface;
87
88         /** The sones to update. */
89         private final Set<Sone> sones = new HashSet<Sone>();
90
91         /**
92          * Creates a new Sone downloader.
93          *
94          * @param core
95          *              The core
96          * @param freenetInterface
97          *              The Freenet interface
98          */
99         public SoneDownloaderImpl(Core core, FreenetInterface freenetInterface) {
100                 super("Sone Downloader", false);
101                 this.core = core;
102                 this.freenetInterface = freenetInterface;
103         }
104
105         //
106         // ACTIONS
107         //
108
109         /**
110          * Adds the given Sone to the set of Sones that will be watched for updates.
111          *
112          * @param sone
113          *              The Sone to add
114          */
115         @Override
116         public void addSone(final Sone sone) {
117                 if (!sones.add(sone)) {
118                         freenetInterface.unregisterUsk(sone);
119                 }
120                 final USKCallback uskCallback = new USKCallback() {
121
122                         @Override
123                         @SuppressWarnings("synthetic-access")
124                         public void onFoundEdition(long edition, USK key,
125                                         ObjectContainer objectContainer,
126                                         ClientContext clientContext, boolean metadata,
127                                         short codec, byte[] data, boolean newKnownGood,
128                                         boolean newSlotToo) {
129                                 logger.log(Level.FINE, format(
130                                                 "Found USK update for Sone “%s” at %s, new known good: %s, new slot too: %s.",
131                                                 sone, key, newKnownGood, newSlotToo));
132                                 if (edition > sone.getLatestEdition()) {
133                                         sone.setLatestEdition(edition);
134                                         new Thread(fetchSoneAction(sone),
135                                                         "Sone Downloader").start();
136                                 }
137                         }
138
139                         @Override
140                         public short getPollingPriorityProgress() {
141                                 return RequestStarter.INTERACTIVE_PRIORITY_CLASS;
142                         }
143
144                         @Override
145                         public short getPollingPriorityNormal() {
146                                 return RequestStarter.INTERACTIVE_PRIORITY_CLASS;
147                         }
148                 };
149                 if (soneHasBeenActiveRecently(sone)) {
150                         freenetInterface.registerActiveUsk(sone.getRequestUri(),
151                                         uskCallback);
152                 } else {
153                         freenetInterface.registerPassiveUsk(sone.getRequestUri(),
154                                         uskCallback);
155                 }
156         }
157
158         private boolean soneHasBeenActiveRecently(Sone sone) {
159                 return (currentTimeMillis() - sone.getTime()) < DAYS.toMillis(7);
160         }
161
162         private void fetchSone(Sone sone) {
163                 fetchSone(sone, sone.getRequestUri().sskForUSK());
164         }
165
166         /**
167          * Fetches the updated Sone. This method can be used to fetch a Sone from a
168          * specific URI.
169          *
170          * @param sone
171          *              The Sone to fetch
172          * @param soneUri
173          *              The URI to fetch the Sone from
174          */
175         @Override
176         public void fetchSone(Sone sone, FreenetURI soneUri) {
177                 fetchSone(sone, soneUri, false);
178         }
179
180         /**
181          * Fetches the Sone from the given URI.
182          *
183          * @param sone
184          *              The Sone to fetch
185          * @param soneUri
186          *              The URI of the Sone to fetch
187          * @param fetchOnly
188          *              {@code true} to only fetch and parse the Sone, {@code false}
189          *              to {@link Core#updateSone(Sone) update} it in the core
190          * @return The downloaded Sone, or {@code null} if the Sone could not be
191          *         downloaded
192          */
193         @Override
194         public Sone fetchSone(Sone sone, FreenetURI soneUri, boolean fetchOnly) {
195                 logger.log(Level.FINE, String.format("Starting fetch for Sone “%s” from %s…", sone, soneUri));
196                 FreenetURI requestUri = soneUri.setMetaString(new String[] { "sone.xml" });
197                 sone.setStatus(SoneStatus.downloading);
198                 try {
199                         Fetched fetchResults = freenetInterface.fetchUri(requestUri);
200                         if (fetchResults == null) {
201                                 /* TODO - mark Sone as bad. */
202                                 return null;
203                         }
204                         logger.log(Level.FINEST, String.format("Got %d bytes back.", fetchResults.getFetchResult().size()));
205                         Sone parsedSone = parseSone(sone, fetchResults.getFetchResult(), fetchResults.getFreenetUri());
206                         if (parsedSone != null) {
207                                 if (!fetchOnly) {
208                                         parsedSone.setStatus((parsedSone.getTime() == 0) ? SoneStatus.unknown : SoneStatus.idle);
209                                         core.updateSone(parsedSone);
210                                         addSone(parsedSone);
211                                 }
212                         }
213                         return parsedSone;
214                 } finally {
215                         sone.setStatus((sone.getTime() == 0) ? SoneStatus.unknown : SoneStatus.idle);
216                 }
217         }
218
219         /**
220          * Parses a Sone from a fetch result.
221          *
222          * @param originalSone
223          *              The sone to parse, or {@code null} if the Sone is yet unknown
224          * @param fetchResult
225          *              The fetch result
226          * @param requestUri
227          *              The requested URI
228          * @return The parsed Sone, or {@code null} if the Sone could not be parsed
229          */
230         private Sone parseSone(Sone originalSone, FetchResult fetchResult, FreenetURI requestUri) {
231                 logger.log(Level.FINEST, String.format("Parsing FetchResult (%d bytes, %s) for %s…", fetchResult.size(), fetchResult.getMimeType(), originalSone));
232                 Bucket soneBucket = fetchResult.asBucket();
233                 InputStream soneInputStream = null;
234                 try {
235                         soneInputStream = soneBucket.getInputStream();
236                         Sone parsedSone = parseSone(originalSone, soneInputStream);
237                         if (parsedSone != null) {
238                                 parsedSone.setLatestEdition(requestUri.getEdition());
239                                 if (requestUri.getKeyType().equals("USK")) {
240                                         parsedSone.setRequestUri(requestUri.setMetaString(new String[0]));
241                                 } else {
242                                         parsedSone.setRequestUri(requestUri.setKeyType("USK").setDocName("Sone").setMetaString(new String[0]));
243                                 }
244                         }
245                         return parsedSone;
246                 } catch (Exception e1) {
247                         logger.log(Level.WARNING, String.format("Could not parse Sone from %s!", requestUri), e1);
248                 } finally {
249                         Closer.close(soneInputStream);
250                         soneBucket.free();
251                 }
252                 return null;
253         }
254
255         /**
256          * Parses a Sone from the given input stream and creates a new Sone from the
257          * parsed data.
258          *
259          * @param originalSone
260          *              The Sone to update
261          * @param soneInputStream
262          *              The input stream to parse the Sone from
263          * @return The parsed Sone
264          * @throws SoneException
265          *              if a parse error occurs, or the protocol is invalid
266          */
267         @VisibleForTesting
268         protected Sone parseSone(Sone originalSone, InputStream soneInputStream) throws SoneException {
269                 /* TODO - impose a size limit? */
270
271                 Document document;
272                 /* XML parsing is not thread-safe. */
273                 synchronized (this) {
274                         document = XML.transformToDocument(soneInputStream);
275                 }
276                 if (document == null) {
277                         /* TODO - mark Sone as bad. */
278                         logger.log(Level.WARNING, String.format("Could not parse XML for Sone %s!", originalSone));
279                         return null;
280                 }
281
282                 SoneBuilder soneBuilder = core.soneBuilder().from(originalSone.getIdentity());
283                 if (originalSone.isLocal()) {
284                         soneBuilder = soneBuilder.local();
285                 }
286                 Sone sone = soneBuilder.build();
287
288                 SimpleXML soneXml;
289                 try {
290                         soneXml = SimpleXML.fromDocument(document);
291                 } catch (NullPointerException npe1) {
292                         /* for some reason, invalid XML can cause NPEs. */
293                         logger.log(Level.WARNING, String.format("XML for Sone %s can not be parsed!", sone), npe1);
294                         return null;
295                 }
296
297                 Integer protocolVersion = null;
298                 String soneProtocolVersion = soneXml.getValue("protocol-version", null);
299                 if (soneProtocolVersion != null) {
300                         protocolVersion = Numbers.safeParseInteger(soneProtocolVersion);
301                 }
302                 if (protocolVersion == null) {
303                         logger.log(Level.INFO, "No protocol version found, assuming 0.");
304                         protocolVersion = 0;
305                 }
306
307                 if (protocolVersion < 0) {
308                         logger.log(Level.WARNING, String.format("Invalid protocol version: %d! Not parsing Sone.", protocolVersion));
309                         return null;
310                 }
311
312                 /* check for valid versions. */
313                 if (protocolVersion > MAX_PROTOCOL_VERSION) {
314                         logger.log(Level.WARNING, String.format("Unknown protocol version: %d! Not parsing Sone.", protocolVersion));
315                         return null;
316                 }
317
318                 String soneTime = soneXml.getValue("time", null);
319                 if (soneTime == null) {
320                         /* TODO - mark Sone as bad. */
321                         logger.log(Level.WARNING, String.format("Downloaded time for Sone %s was null!", sone));
322                         return null;
323                 }
324                 try {
325                         sone.setTime(Long.parseLong(soneTime));
326                 } catch (NumberFormatException nfe1) {
327                         /* TODO - mark Sone as bad. */
328                         logger.log(Level.WARNING, String.format("Downloaded Sone %s with invalid time: %s", sone, soneTime));
329                         return null;
330                 }
331
332                 SimpleXML clientXml = soneXml.getNode("client");
333                 if (clientXml != null) {
334                         String clientName = clientXml.getValue("name", null);
335                         String clientVersion = clientXml.getValue("version", null);
336                         if ((clientName == null) || (clientVersion == null)) {
337                                 logger.log(Level.WARNING, String.format("Download Sone %s with client XML but missing name or version!", sone));
338                                 return null;
339                         }
340                         sone.setClient(new Client(clientName, clientVersion));
341                 }
342
343                 String soneRequestUri = soneXml.getValue("request-uri", null);
344                 if (soneRequestUri != null) {
345                         try {
346                                 sone.setRequestUri(new FreenetURI(soneRequestUri));
347                         } catch (MalformedURLException mue1) {
348                                 /* TODO - mark Sone as bad. */
349                                 logger.log(Level.WARNING, String.format("Downloaded Sone %s has invalid request URI: %s", sone, soneRequestUri), mue1);
350                                 return null;
351                         }
352                 }
353
354                 SimpleXML profileXml = soneXml.getNode("profile");
355                 if (profileXml == null) {
356                         /* TODO - mark Sone as bad. */
357                         logger.log(Level.WARNING, String.format("Downloaded Sone %s has no profile!", sone));
358                         return null;
359                 }
360
361                 /* parse profile. */
362                 String profileFirstName = profileXml.getValue("first-name", null);
363                 String profileMiddleName = profileXml.getValue("middle-name", null);
364                 String profileLastName = profileXml.getValue("last-name", null);
365                 Integer profileBirthDay = Numbers.safeParseInteger(profileXml.getValue("birth-day", null));
366                 Integer profileBirthMonth = Numbers.safeParseInteger(profileXml.getValue("birth-month", null));
367                 Integer profileBirthYear = Numbers.safeParseInteger(profileXml.getValue("birth-year", null));
368                 Profile profile = new Profile(sone).setFirstName(profileFirstName).setMiddleName(profileMiddleName).setLastName(profileLastName);
369                 profile.setBirthDay(profileBirthDay).setBirthMonth(profileBirthMonth).setBirthYear(profileBirthYear);
370                 /* avatar is processed after images are loaded. */
371                 String avatarId = profileXml.getValue("avatar", null);
372
373                 /* parse profile fields. */
374                 SimpleXML profileFieldsXml = profileXml.getNode("fields");
375                 if (profileFieldsXml != null) {
376                         for (SimpleXML fieldXml : profileFieldsXml.getNodes("field")) {
377                                 String fieldName = fieldXml.getValue("field-name", null);
378                                 String fieldValue = fieldXml.getValue("field-value", "");
379                                 if (fieldName == null) {
380                                         logger.log(Level.WARNING, String.format("Downloaded profile field for Sone %s with missing data! Name: %s, Value: %s", sone, fieldName, fieldValue));
381                                         return null;
382                                 }
383                                 try {
384                                         profile.addField(fieldName.trim()).setValue(fieldValue);
385                                 } catch (IllegalArgumentException iae1) {
386                                         logger.log(Level.WARNING, String.format("Duplicate field: %s", fieldName), iae1);
387                                         return null;
388                                 }
389                         }
390                 }
391
392                 /* parse posts. */
393                 SimpleXML postsXml = soneXml.getNode("posts");
394                 Set<Post> posts = new HashSet<Post>();
395                 if (postsXml == null) {
396                         /* TODO - mark Sone as bad. */
397                         logger.log(Level.WARNING, String.format("Downloaded Sone %s has no posts!", sone));
398                 } else {
399                         for (SimpleXML postXml : postsXml.getNodes("post")) {
400                                 String postId = postXml.getValue("id", null);
401                                 String postRecipientId = postXml.getValue("recipient", null);
402                                 String postTime = postXml.getValue("time", null);
403                                 String postText = postXml.getValue("text", null);
404                                 if ((postId == null) || (postTime == null) || (postText == null)) {
405                                         /* TODO - mark Sone as bad. */
406                                         logger.log(Level.WARNING, String.format("Downloaded post for Sone %s with missing data! ID: %s, Time: %s, Text: %s", sone, postId, postTime, postText));
407                                         return null;
408                                 }
409                                 try {
410                                         PostBuilder postBuilder = core.postBuilder();
411                                         /* TODO - parse time correctly. */
412                                         postBuilder.withId(postId).from(sone.getId()).withTime(Long.parseLong(postTime)).withText(postText);
413                                         if ((postRecipientId != null) && (postRecipientId.length() == 43)) {
414                                                 postBuilder.to(postRecipientId);
415                                         }
416                                         posts.add(postBuilder.build());
417                                 } catch (NumberFormatException nfe1) {
418                                         /* TODO - mark Sone as bad. */
419                                         logger.log(Level.WARNING, String.format("Downloaded post for Sone %s with invalid time: %s", sone, postTime));
420                                         return null;
421                                 }
422                         }
423                 }
424
425                 /* parse replies. */
426                 SimpleXML repliesXml = soneXml.getNode("replies");
427                 Set<PostReply> replies = new HashSet<PostReply>();
428                 if (repliesXml == null) {
429                         /* TODO - mark Sone as bad. */
430                         logger.log(Level.WARNING, String.format("Downloaded Sone %s has no replies!", sone));
431                 } else {
432                         for (SimpleXML replyXml : repliesXml.getNodes("reply")) {
433                                 String replyId = replyXml.getValue("id", null);
434                                 String replyPostId = replyXml.getValue("post-id", null);
435                                 String replyTime = replyXml.getValue("time", null);
436                                 String replyText = replyXml.getValue("text", null);
437                                 if ((replyId == null) || (replyPostId == null) || (replyTime == null) || (replyText == null)) {
438                                         /* TODO - mark Sone as bad. */
439                                         logger.log(Level.WARNING, String.format("Downloaded reply for Sone %s with missing data! ID: %s, Post: %s, Time: %s, Text: %s", sone, replyId, replyPostId, replyTime, replyText));
440                                         return null;
441                                 }
442                                 try {
443                                         PostReplyBuilder postReplyBuilder = core.postReplyBuilder();
444                                         /* TODO - parse time correctly. */
445                                         postReplyBuilder.withId(replyId).from(sone.getId()).to(replyPostId).withTime(Long.parseLong(replyTime)).withText(replyText);
446                                         replies.add(postReplyBuilder.build());
447                                 } catch (NumberFormatException nfe1) {
448                                         /* TODO - mark Sone as bad. */
449                                         logger.log(Level.WARNING, String.format("Downloaded reply for Sone %s with invalid time: %s", sone, replyTime));
450                                         return null;
451                                 }
452                         }
453                 }
454
455                 /* parse liked post IDs. */
456                 SimpleXML likePostIdsXml = soneXml.getNode("post-likes");
457                 Set<String> likedPostIds = new HashSet<String>();
458                 if (likePostIdsXml == null) {
459                         /* TODO - mark Sone as bad. */
460                         logger.log(Level.WARNING, String.format("Downloaded Sone %s has no post likes!", sone));
461                 } else {
462                         for (SimpleXML likedPostIdXml : likePostIdsXml.getNodes("post-like")) {
463                                 String postId = likedPostIdXml.getValue();
464                                 likedPostIds.add(postId);
465                         }
466                 }
467
468                 /* parse liked reply IDs. */
469                 SimpleXML likeReplyIdsXml = soneXml.getNode("reply-likes");
470                 Set<String> likedReplyIds = new HashSet<String>();
471                 if (likeReplyIdsXml == null) {
472                         /* TODO - mark Sone as bad. */
473                         logger.log(Level.WARNING, String.format("Downloaded Sone %s has no reply likes!", sone));
474                 } else {
475                         for (SimpleXML likedReplyIdXml : likeReplyIdsXml.getNodes("reply-like")) {
476                                 String replyId = likedReplyIdXml.getValue();
477                                 likedReplyIds.add(replyId);
478                         }
479                 }
480
481                 /* parse albums. */
482                 SimpleXML albumsXml = soneXml.getNode("albums");
483                 Map<String, Image> allImages = new HashMap<String, Image>();
484                 List<Album> topLevelAlbums = new ArrayList<Album>();
485                 if (albumsXml != null) {
486                         for (SimpleXML albumXml : albumsXml.getNodes("album")) {
487                                 String id = albumXml.getValue("id", null);
488                                 String parentId = albumXml.getValue("parent", null);
489                                 String title = albumXml.getValue("title", null);
490                                 String description = albumXml.getValue("description", "");
491                                 String albumImageId = albumXml.getValue("album-image", null);
492                                 if ((id == null) || (title == null)) {
493                                         logger.log(Level.WARNING, String.format("Downloaded Sone %s contains invalid album!", sone));
494                                         return null;
495                                 }
496                                 Album parent = null;
497                                 if (parentId != null) {
498                                         parent = core.getAlbum(parentId);
499                                         if (parent == null) {
500                                                 logger.log(Level.WARNING, String.format("Downloaded Sone %s has album with invalid parent!", sone));
501                                                 return null;
502                                         }
503                                 }
504                                 Album album = core.albumBuilder()
505                                                 .withId(id)
506                                                 .by(sone)
507                                                 .build()
508                                                 .modify()
509                                                 .setTitle(title)
510                                                 .setDescription(description)
511                                                 .update();
512                                 if (parent != null) {
513                                         parent.addAlbum(album);
514                                 } else {
515                                         topLevelAlbums.add(album);
516                                 }
517                                 SimpleXML imagesXml = albumXml.getNode("images");
518                                 if (imagesXml != null) {
519                                         for (SimpleXML imageXml : imagesXml.getNodes("image")) {
520                                                 String imageId = imageXml.getValue("id", null);
521                                                 String imageCreationTimeString = imageXml.getValue("creation-time", null);
522                                                 String imageKey = imageXml.getValue("key", null);
523                                                 String imageTitle = imageXml.getValue("title", null);
524                                                 String imageDescription = imageXml.getValue("description", "");
525                                                 String imageWidthString = imageXml.getValue("width", null);
526                                                 String imageHeightString = imageXml.getValue("height", null);
527                                                 if ((imageId == null) || (imageCreationTimeString == null) || (imageKey == null) || (imageTitle == null) || (imageWidthString == null) || (imageHeightString == null)) {
528                                                         logger.log(Level.WARNING, String.format("Downloaded Sone %s contains invalid images!", sone));
529                                                         return null;
530                                                 }
531                                                 long creationTime = Numbers.safeParseLong(imageCreationTimeString, 0L);
532                                                 int imageWidth = Numbers.safeParseInteger(imageWidthString, 0);
533                                                 int imageHeight = Numbers.safeParseInteger(imageHeightString, 0);
534                                                 if ((imageWidth < 1) || (imageHeight < 1)) {
535                                                         logger.log(Level.WARNING, String.format("Downloaded Sone %s contains image %s with invalid dimensions (%s, %s)!", sone, imageId, imageWidthString, imageHeightString));
536                                                         return null;
537                                                 }
538                                                 Image image = core.imageBuilder().withId(imageId).build().modify().setSone(sone).setKey(imageKey).setCreationTime(creationTime).update();
539                                                 image = image.modify().setTitle(imageTitle).setDescription(imageDescription).update();
540                                                 image = image.modify().setWidth(imageWidth).setHeight(imageHeight).update();
541                                                 album.addImage(image);
542                                                 allImages.put(imageId, image);
543                                         }
544                                 }
545                                 album.modify().setAlbumImage(albumImageId).update();
546                         }
547                 }
548
549                 /* process avatar. */
550                 if (avatarId != null) {
551                         profile.setAvatar(allImages.get(avatarId));
552                 }
553
554                 /* okay, apparently everything was parsed correctly. Now import. */
555                 /* atomic setter operation on the Sone. */
556                 synchronized (sone) {
557                         sone.setProfile(profile);
558                         sone.setPosts(posts);
559                         sone.setReplies(replies);
560                         sone.setLikePostIds(likedPostIds);
561                         sone.setLikeReplyIds(likedReplyIds);
562                         for (Album album : topLevelAlbums) {
563                                 sone.getRootAlbum().addAlbum(album);
564                         }
565                 }
566
567                 return sone;
568         }
569
570         @Override
571         public Runnable fetchSoneWithUriAction(final Sone sone) {
572                 return new Runnable() {
573                         @Override
574                         public void run() {
575                                 fetchSone(sone, sone.getRequestUri());
576                         }
577                 };
578         }
579
580         @Override
581         public Runnable fetchSoneAction(final Sone sone) {
582                 return new Runnable() {
583                         @Override
584                         public void run() {
585                                 fetchSone(sone);
586                         }
587                 };
588         }
589
590         /** {@inheritDoc} */
591         @Override
592         protected void serviceStop() {
593                 for (Sone sone : sones) {
594                         freenetInterface.unregisterUsk(sone);
595                 }
596         }
597
598 }