Don’t set insert URI of a Sone, let it be generated from the identity.
[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                 SimpleXML profileXml = soneXml.getNode("profile");
350                 if (profileXml == null) {
351                         /* TODO - mark Sone as bad. */
352                         logger.log(Level.WARNING, String.format("Downloaded Sone %s has no profile!", sone));
353                         return null;
354                 }
355
356                 /* parse profile. */
357                 String profileFirstName = profileXml.getValue("first-name", null);
358                 String profileMiddleName = profileXml.getValue("middle-name", null);
359                 String profileLastName = profileXml.getValue("last-name", null);
360                 Integer profileBirthDay = Numbers.safeParseInteger(profileXml.getValue("birth-day", null));
361                 Integer profileBirthMonth = Numbers.safeParseInteger(profileXml.getValue("birth-month", null));
362                 Integer profileBirthYear = Numbers.safeParseInteger(profileXml.getValue("birth-year", null));
363                 Profile profile = new Profile(sone).setFirstName(profileFirstName).setMiddleName(profileMiddleName).setLastName(profileLastName);
364                 profile.setBirthDay(profileBirthDay).setBirthMonth(profileBirthMonth).setBirthYear(profileBirthYear);
365                 /* avatar is processed after images are loaded. */
366                 String avatarId = profileXml.getValue("avatar", null);
367
368                 /* parse profile fields. */
369                 SimpleXML profileFieldsXml = profileXml.getNode("fields");
370                 if (profileFieldsXml != null) {
371                         for (SimpleXML fieldXml : profileFieldsXml.getNodes("field")) {
372                                 String fieldName = fieldXml.getValue("field-name", null);
373                                 String fieldValue = fieldXml.getValue("field-value", "");
374                                 if (fieldName == null) {
375                                         logger.log(Level.WARNING, String.format("Downloaded profile field for Sone %s with missing data! Name: %s, Value: %s", sone, fieldName, fieldValue));
376                                         return null;
377                                 }
378                                 try {
379                                         profile.addField(fieldName.trim()).setValue(fieldValue);
380                                 } catch (IllegalArgumentException iae1) {
381                                         logger.log(Level.WARNING, String.format("Duplicate field: %s", fieldName), iae1);
382                                         return null;
383                                 }
384                         }
385                 }
386
387                 /* parse posts. */
388                 SimpleXML postsXml = soneXml.getNode("posts");
389                 Set<Post> posts = new HashSet<Post>();
390                 if (postsXml == null) {
391                         /* TODO - mark Sone as bad. */
392                         logger.log(Level.WARNING, String.format("Downloaded Sone %s has no posts!", sone));
393                 } else {
394                         for (SimpleXML postXml : postsXml.getNodes("post")) {
395                                 String postId = postXml.getValue("id", null);
396                                 String postRecipientId = postXml.getValue("recipient", null);
397                                 String postTime = postXml.getValue("time", null);
398                                 String postText = postXml.getValue("text", null);
399                                 if ((postId == null) || (postTime == null) || (postText == null)) {
400                                         /* TODO - mark Sone as bad. */
401                                         logger.log(Level.WARNING, String.format("Downloaded post for Sone %s with missing data! ID: %s, Time: %s, Text: %s", sone, postId, postTime, postText));
402                                         return null;
403                                 }
404                                 try {
405                                         PostBuilder postBuilder = core.postBuilder();
406                                         /* TODO - parse time correctly. */
407                                         postBuilder.withId(postId).from(sone.getId()).withTime(Long.parseLong(postTime)).withText(postText);
408                                         if ((postRecipientId != null) && (postRecipientId.length() == 43)) {
409                                                 postBuilder.to(postRecipientId);
410                                         }
411                                         posts.add(postBuilder.build());
412                                 } catch (NumberFormatException nfe1) {
413                                         /* TODO - mark Sone as bad. */
414                                         logger.log(Level.WARNING, String.format("Downloaded post for Sone %s with invalid time: %s", sone, postTime));
415                                         return null;
416                                 }
417                         }
418                 }
419
420                 /* parse replies. */
421                 SimpleXML repliesXml = soneXml.getNode("replies");
422                 Set<PostReply> replies = new HashSet<PostReply>();
423                 if (repliesXml == null) {
424                         /* TODO - mark Sone as bad. */
425                         logger.log(Level.WARNING, String.format("Downloaded Sone %s has no replies!", sone));
426                 } else {
427                         for (SimpleXML replyXml : repliesXml.getNodes("reply")) {
428                                 String replyId = replyXml.getValue("id", null);
429                                 String replyPostId = replyXml.getValue("post-id", null);
430                                 String replyTime = replyXml.getValue("time", null);
431                                 String replyText = replyXml.getValue("text", null);
432                                 if ((replyId == null) || (replyPostId == null) || (replyTime == null) || (replyText == null)) {
433                                         /* TODO - mark Sone as bad. */
434                                         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));
435                                         return null;
436                                 }
437                                 try {
438                                         PostReplyBuilder postReplyBuilder = core.postReplyBuilder();
439                                         /* TODO - parse time correctly. */
440                                         postReplyBuilder.withId(replyId).from(sone.getId()).to(replyPostId).withTime(Long.parseLong(replyTime)).withText(replyText);
441                                         replies.add(postReplyBuilder.build());
442                                 } catch (NumberFormatException nfe1) {
443                                         /* TODO - mark Sone as bad. */
444                                         logger.log(Level.WARNING, String.format("Downloaded reply for Sone %s with invalid time: %s", sone, replyTime));
445                                         return null;
446                                 }
447                         }
448                 }
449
450                 /* parse liked post IDs. */
451                 SimpleXML likePostIdsXml = soneXml.getNode("post-likes");
452                 Set<String> likedPostIds = new HashSet<String>();
453                 if (likePostIdsXml == null) {
454                         /* TODO - mark Sone as bad. */
455                         logger.log(Level.WARNING, String.format("Downloaded Sone %s has no post likes!", sone));
456                 } else {
457                         for (SimpleXML likedPostIdXml : likePostIdsXml.getNodes("post-like")) {
458                                 String postId = likedPostIdXml.getValue();
459                                 likedPostIds.add(postId);
460                         }
461                 }
462
463                 /* parse liked reply IDs. */
464                 SimpleXML likeReplyIdsXml = soneXml.getNode("reply-likes");
465                 Set<String> likedReplyIds = new HashSet<String>();
466                 if (likeReplyIdsXml == null) {
467                         /* TODO - mark Sone as bad. */
468                         logger.log(Level.WARNING, String.format("Downloaded Sone %s has no reply likes!", sone));
469                 } else {
470                         for (SimpleXML likedReplyIdXml : likeReplyIdsXml.getNodes("reply-like")) {
471                                 String replyId = likedReplyIdXml.getValue();
472                                 likedReplyIds.add(replyId);
473                         }
474                 }
475
476                 /* parse albums. */
477                 SimpleXML albumsXml = soneXml.getNode("albums");
478                 Map<String, Image> allImages = new HashMap<String, Image>();
479                 List<Album> topLevelAlbums = new ArrayList<Album>();
480                 if (albumsXml != null) {
481                         for (SimpleXML albumXml : albumsXml.getNodes("album")) {
482                                 String id = albumXml.getValue("id", null);
483                                 String parentId = albumXml.getValue("parent", null);
484                                 String title = albumXml.getValue("title", null);
485                                 String description = albumXml.getValue("description", "");
486                                 String albumImageId = albumXml.getValue("album-image", null);
487                                 if ((id == null) || (title == null)) {
488                                         logger.log(Level.WARNING, String.format("Downloaded Sone %s contains invalid album!", sone));
489                                         return null;
490                                 }
491                                 Album parent = null;
492                                 if (parentId != null) {
493                                         parent = core.getAlbum(parentId);
494                                         if (parent == null) {
495                                                 logger.log(Level.WARNING, String.format("Downloaded Sone %s has album with invalid parent!", sone));
496                                                 return null;
497                                         }
498                                 }
499                                 Album album = core.albumBuilder()
500                                                 .withId(id)
501                                                 .by(sone)
502                                                 .build()
503                                                 .modify()
504                                                 .setTitle(title)
505                                                 .setDescription(description)
506                                                 .update();
507                                 if (parent != null) {
508                                         parent.addAlbum(album);
509                                 } else {
510                                         topLevelAlbums.add(album);
511                                 }
512                                 SimpleXML imagesXml = albumXml.getNode("images");
513                                 if (imagesXml != null) {
514                                         for (SimpleXML imageXml : imagesXml.getNodes("image")) {
515                                                 String imageId = imageXml.getValue("id", null);
516                                                 String imageCreationTimeString = imageXml.getValue("creation-time", null);
517                                                 String imageKey = imageXml.getValue("key", null);
518                                                 String imageTitle = imageXml.getValue("title", null);
519                                                 String imageDescription = imageXml.getValue("description", "");
520                                                 String imageWidthString = imageXml.getValue("width", null);
521                                                 String imageHeightString = imageXml.getValue("height", null);
522                                                 if ((imageId == null) || (imageCreationTimeString == null) || (imageKey == null) || (imageTitle == null) || (imageWidthString == null) || (imageHeightString == null)) {
523                                                         logger.log(Level.WARNING, String.format("Downloaded Sone %s contains invalid images!", sone));
524                                                         return null;
525                                                 }
526                                                 long creationTime = Numbers.safeParseLong(imageCreationTimeString, 0L);
527                                                 int imageWidth = Numbers.safeParseInteger(imageWidthString, 0);
528                                                 int imageHeight = Numbers.safeParseInteger(imageHeightString, 0);
529                                                 if ((imageWidth < 1) || (imageHeight < 1)) {
530                                                         logger.log(Level.WARNING, String.format("Downloaded Sone %s contains image %s with invalid dimensions (%s, %s)!", sone, imageId, imageWidthString, imageHeightString));
531                                                         return null;
532                                                 }
533                                                 Image image = core.imageBuilder().withId(imageId).build().modify().setSone(sone).setKey(imageKey).setCreationTime(creationTime).update();
534                                                 image = image.modify().setTitle(imageTitle).setDescription(imageDescription).update();
535                                                 image = image.modify().setWidth(imageWidth).setHeight(imageHeight).update();
536                                                 album.addImage(image);
537                                                 allImages.put(imageId, image);
538                                         }
539                                 }
540                                 album.modify().setAlbumImage(albumImageId).update();
541                         }
542                 }
543
544                 /* process avatar. */
545                 if (avatarId != null) {
546                         profile.setAvatar(allImages.get(avatarId));
547                 }
548
549                 /* okay, apparently everything was parsed correctly. Now import. */
550                 /* atomic setter operation on the Sone. */
551                 synchronized (sone) {
552                         sone.setProfile(profile);
553                         sone.setPosts(posts);
554                         sone.setReplies(replies);
555                         sone.setLikePostIds(likedPostIds);
556                         sone.setLikeReplyIds(likedReplyIds);
557                         for (Album album : topLevelAlbums) {
558                                 sone.getRootAlbum().addAlbum(album);
559                         }
560                 }
561
562                 return sone;
563         }
564
565         @Override
566         public Runnable fetchSoneWithUriAction(final Sone sone) {
567                 return new Runnable() {
568                         @Override
569                         public void run() {
570                                 fetchSone(sone, sone.getRequestUri());
571                         }
572                 };
573         }
574
575         @Override
576         public Runnable fetchSoneAction(final Sone sone) {
577                 return new Runnable() {
578                         @Override
579                         public void run() {
580                                 fetchSone(sone);
581                         }
582                 };
583         }
584
585         /** {@inheritDoc} */
586         @Override
587         protected void serviceStop() {
588                 for (Sone sone : sones) {
589                         freenetInterface.unregisterUsk(sone);
590                 }
591         }
592
593 }