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