Merge branch 'release-0.9.7'
[Sone.git] / src / main / java / net / pterodactylus / sone / core / FreenetInterface.java
1 /*
2  * Sone - FreenetInterface.java - Copyright © 2010–2016 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 freenet.keys.USK.create;
21 import static java.lang.String.format;
22 import static java.util.logging.Level.WARNING;
23 import static java.util.logging.Logger.getLogger;
24 import static net.pterodactylus.sone.freenet.Key.routingKey;
25
26 import java.io.IOException;
27 import java.net.MalformedURLException;
28 import java.util.Collections;
29 import java.util.HashMap;
30 import java.util.Map;
31 import java.util.logging.Level;
32 import java.util.logging.Logger;
33
34 import javax.annotation.Nonnull;
35
36 import net.pterodactylus.sone.core.event.ImageInsertAbortedEvent;
37 import net.pterodactylus.sone.core.event.ImageInsertFailedEvent;
38 import net.pterodactylus.sone.core.event.ImageInsertFinishedEvent;
39 import net.pterodactylus.sone.core.event.ImageInsertStartedEvent;
40 import net.pterodactylus.sone.data.Image;
41 import net.pterodactylus.sone.data.Sone;
42 import net.pterodactylus.sone.data.TemporaryImage;
43
44 import com.google.common.base.Function;
45 import com.google.common.eventbus.EventBus;
46 import com.google.inject.Inject;
47 import com.google.inject.Singleton;
48
49 import freenet.client.ClientMetadata;
50 import freenet.client.FetchContext;
51 import freenet.client.FetchException;
52 import freenet.client.FetchException.FetchExceptionMode;
53 import freenet.client.FetchResult;
54 import freenet.client.HighLevelSimpleClient;
55 import freenet.client.InsertBlock;
56 import freenet.client.InsertContext;
57 import freenet.client.InsertException;
58 import freenet.client.Metadata;
59 import freenet.client.async.BaseClientPutter;
60 import freenet.client.async.ClientContext;
61 import freenet.client.async.ClientGetCallback;
62 import freenet.client.async.ClientGetter;
63 import freenet.client.async.ClientPutCallback;
64 import freenet.client.async.ClientPutter;
65 import freenet.client.async.SnoopMetadata;
66 import freenet.client.async.USKCallback;
67 import freenet.keys.FreenetURI;
68 import freenet.keys.InsertableClientSSK;
69 import freenet.keys.USK;
70 import freenet.node.Node;
71 import freenet.node.RequestClient;
72 import freenet.node.RequestClientBuilder;
73 import freenet.node.RequestStarter;
74 import freenet.support.api.Bucket;
75 import freenet.support.api.RandomAccessBucket;
76 import freenet.support.io.ArrayBucket;
77 import freenet.support.io.ResumeFailedException;
78
79 /**
80  * Contains all necessary functionality for interacting with the Freenet node.
81  *
82  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
83  */
84 @Singleton
85 public class FreenetInterface {
86
87         /** The logger. */
88         private static final Logger logger = getLogger(FreenetInterface.class.getName());
89
90         /** The event bus. */
91         private final EventBus eventBus;
92
93         /** The node to interact with. */
94         private final Node node;
95
96         /** The high-level client to use for requests. */
97         private final HighLevelSimpleClient client;
98
99         /** The USK callbacks. */
100         private final Map<String, USKCallback> soneUskCallbacks = new HashMap<String, USKCallback>();
101
102         /** The not-Sone-related USK callbacks. */
103         private final Map<FreenetURI, USKCallback> uriUskCallbacks = Collections.synchronizedMap(new HashMap<FreenetURI, USKCallback>());
104
105         private final RequestClient imageInserts = new RequestClientBuilder().realTime().build();
106         private final RequestClient imageLoader = new RequestClientBuilder().realTime().build();
107
108         /**
109          * Creates a new Freenet interface.
110          *
111          * @param eventBus
112          *            The event bus
113          * @param node
114          *            The node to interact with
115          */
116         @Inject
117         public FreenetInterface(EventBus eventBus, Node node) {
118                 this.eventBus = eventBus;
119                 this.node = node;
120                 this.client = node.clientCore.makeClient(RequestStarter.INTERACTIVE_PRIORITY_CLASS, false, true);
121         }
122
123         //
124         // ACTIONS
125         //
126
127         /**
128          * Fetches the given URI.
129          *
130          * @param uri
131          *            The URI to fetch
132          * @return The result of the fetch, or {@code null} if an error occured
133          */
134         public Fetched fetchUri(FreenetURI uri) {
135                 FreenetURI currentUri = new FreenetURI(uri);
136                 while (true) {
137                         try {
138                                 FetchResult fetchResult = client.fetch(currentUri);
139                                 return new Fetched(currentUri, fetchResult);
140                         } catch (FetchException fe1) {
141                                 if (fe1.getMode() == FetchExceptionMode.PERMANENT_REDIRECT) {
142                                         currentUri = fe1.newURI;
143                                         continue;
144                                 }
145                                 logger.log(Level.WARNING, String.format("Could not fetch “%s”!", uri), fe1);
146                                 return null;
147                         }
148                 }
149         }
150
151         public void startFetch(final FreenetURI uri, final BackgroundFetchCallback backgroundFetchCallback) {
152                 ClientGetCallback callback = new ClientGetCallback() {
153                         @Override
154                         public void onSuccess(FetchResult result, ClientGetter state) {
155                                 try {
156                                         backgroundFetchCallback.loaded(uri, result.getMimeType(), result.asByteArray());
157                                 } catch (IOException e) {
158                                         backgroundFetchCallback.failed(uri);
159                                 }
160                         }
161
162                         @Override
163                         public void onFailure(FetchException e, ClientGetter state) {
164                                 backgroundFetchCallback.failed(uri);
165                         }
166
167                         @Override
168                         public void onResume(ClientContext context) throws ResumeFailedException {
169                                 /* do nothing. */
170                         }
171
172                         @Override
173                         public RequestClient getRequestClient() {
174                                 return imageLoader;
175                         }
176                 };
177                 SnoopMetadata snoop = new SnoopMetadata() {
178                         @Override
179                         public boolean snoopMetadata(Metadata meta, ClientContext context) {
180                                 String mimeType = meta.getMIMEType();
181                                 boolean cancel = (mimeType == null) || backgroundFetchCallback.shouldCancel(uri, mimeType, meta.dataLength());
182                                 if (cancel) {
183                                         backgroundFetchCallback.failed(uri);
184                                 }
185                                 return cancel;
186                         }
187                 };
188                 FetchContext fetchContext = client.getFetchContext();
189                 try {
190                         ClientGetter clientGetter = client.fetch(uri, 2097152, callback, fetchContext, RequestStarter.INTERACTIVE_PRIORITY_CLASS);
191                         clientGetter.setMetaSnoop(snoop);
192                         clientGetter.restart(uri, fetchContext.filterData, node.clientCore.clientContext);
193                 } catch (FetchException fe) {
194                         /* stupid exception that can not actually be thrown! */
195                 }
196         }
197
198         public interface BackgroundFetchCallback {
199                 boolean shouldCancel(@Nonnull FreenetURI uri, @Nonnull String mimeType, long size);
200                 void loaded(@Nonnull FreenetURI uri, @Nonnull String mimeType, @Nonnull byte[] data);
201                 void failed(@Nonnull FreenetURI uri);
202         }
203
204         /**
205          * Inserts the image data of the given {@link TemporaryImage} and returns
206          * the given insert token that can be used to add listeners or cancel the
207          * insert.
208          *
209          * @param temporaryImage
210          *            The temporary image data
211          * @param image
212          *            The image
213          * @param insertToken
214          *            The insert token
215          * @throws SoneException
216          *             if the insert could not be started
217          */
218         public void insertImage(TemporaryImage temporaryImage, Image image, InsertToken insertToken) throws SoneException {
219                 String filenameHint = image.getId() + "." + temporaryImage.getMimeType().substring(temporaryImage.getMimeType().lastIndexOf("/") + 1);
220                 InsertableClientSSK key = InsertableClientSSK.createRandom(node.random, "");
221                 FreenetURI targetUri = key.getInsertURI().setDocName(filenameHint);
222                 InsertContext insertContext = client.getInsertContext(true);
223                 RandomAccessBucket bucket = new ArrayBucket(temporaryImage.getImageData());
224                 insertToken.setBucket(bucket);
225                 ClientMetadata metadata = new ClientMetadata(temporaryImage.getMimeType());
226                 InsertBlock insertBlock = new InsertBlock(bucket, metadata, targetUri);
227                 try {
228                         ClientPutter clientPutter = client.insert(insertBlock, null, false, insertContext, insertToken, RequestStarter.INTERACTIVE_PRIORITY_CLASS);
229                         insertToken.setClientPutter(clientPutter);
230                 } catch (InsertException ie1) {
231                         throw new SoneInsertException("Could not start image insert.", ie1);
232                 }
233         }
234
235         /**
236          * Inserts a directory into Freenet.
237          *
238          * @param insertUri
239          *            The insert URI
240          * @param manifestEntries
241          *            The directory entries
242          * @param defaultFile
243          *            The name of the default file
244          * @return The generated URI
245          * @throws SoneException
246          *             if an insert error occurs
247          */
248         public FreenetURI insertDirectory(FreenetURI insertUri, HashMap<String, Object> manifestEntries, String defaultFile) throws SoneException {
249                 try {
250                         return client.insertManifest(insertUri, manifestEntries, defaultFile);
251                 } catch (InsertException ie1) {
252                         throw new SoneException(ie1);
253                 }
254         }
255
256         public void registerActiveUsk(FreenetURI requestUri,
257                         USKCallback uskCallback) {
258                 try {
259                         soneUskCallbacks.put(routingKey(requestUri), uskCallback);
260                         node.clientCore.uskManager.subscribe(create(requestUri),
261                                         uskCallback, true, (RequestClient) client);
262                 } catch (MalformedURLException mue1) {
263                         logger.log(WARNING, format("Could not subscribe USK “%s”!",
264                                         requestUri), mue1);
265                 }
266         }
267
268         public void registerPassiveUsk(FreenetURI requestUri,
269                         USKCallback uskCallback) {
270                 try {
271                         soneUskCallbacks.put(routingKey(requestUri), uskCallback);
272                         node.clientCore
273                                         .uskManager
274                                         .subscribe(create(requestUri), uskCallback, false,
275                                                         (RequestClient) client);
276                 } catch (MalformedURLException mue1) {
277                         logger.log(WARNING,
278                                         format("Could not subscribe USK “%s”!", requestUri),
279                                         mue1);
280                 }
281         }
282
283         /**
284          * Unsubscribes the request URI of the given Sone.
285          *
286          * @param sone
287          *            The Sone to unregister
288          */
289         public void unregisterUsk(Sone sone) {
290                 USKCallback uskCallback = soneUskCallbacks.remove(sone.getId());
291                 if (uskCallback == null) {
292                         return;
293                 }
294                 try {
295                         logger.log(Level.FINEST, String.format("Unsubscribing from USK for %s…", sone));
296                         node.clientCore.uskManager.unsubscribe(USK.create(sone.getRequestUri()), uskCallback);
297                 } catch (MalformedURLException mue1) {
298                         logger.log(Level.FINE, String.format("Could not unsubscribe USK “%s”!", sone.getRequestUri()), mue1);
299                 }
300         }
301
302         /**
303          * Registers an arbitrary URI and calls the given callback if a new edition
304          * is found.
305          *
306          * @param uri
307          *            The URI to watch
308          * @param callback
309          *            The callback to call
310          */
311         public void registerUsk(FreenetURI uri, final Callback callback) {
312                 USKCallback uskCallback = new USKCallback() {
313
314                         @Override
315                         public void onFoundEdition(long edition, USK key, ClientContext clientContext, boolean metadata, short codec, byte[] data, boolean newKnownGood, boolean newSlotToo) {
316                                 callback.editionFound(key.getURI(), edition, newKnownGood, newSlotToo);
317                         }
318
319                         @Override
320                         public short getPollingPriorityNormal() {
321                                 return RequestStarter.PREFETCH_PRIORITY_CLASS;
322                         }
323
324                         @Override
325                         public short getPollingPriorityProgress() {
326                                 return RequestStarter.INTERACTIVE_PRIORITY_CLASS;
327                         }
328
329                 };
330                 try {
331                         node.clientCore.uskManager.subscribe(USK.create(uri), uskCallback, true, (RequestClient) client);
332                         uriUskCallbacks.put(uri, uskCallback);
333                 } catch (MalformedURLException mue1) {
334                         logger.log(Level.WARNING, String.format("Could not subscribe to USK: %s", uri), mue1);
335                 }
336         }
337
338         /**
339          * Unregisters the USK watcher for the given URI.
340          *
341          * @param uri
342          *            The URI to unregister the USK watcher for
343          */
344         public void unregisterUsk(FreenetURI uri) {
345                 USKCallback uskCallback = uriUskCallbacks.remove(uri);
346                 if (uskCallback == null) {
347                         logger.log(Level.INFO, String.format("Could not unregister unknown USK: %s", uri));
348                         return;
349                 }
350                 try {
351                         node.clientCore.uskManager.unsubscribe(USK.create(uri), uskCallback);
352                 } catch (MalformedURLException mue1) {
353                         logger.log(Level.INFO, String.format("Could not unregister invalid USK: %s", uri), mue1);
354                 }
355         }
356
357         /**
358          * Container for a fetched URI and the {@link FetchResult}.
359          *
360          * @author <a href="mailto:d.roden@xplosion.de">David Roden</a>
361          */
362         public static class Fetched {
363
364                 /** The fetched URI. */
365                 private final FreenetURI freenetUri;
366
367                 /** The fetch result. */
368                 private final FetchResult fetchResult;
369
370                 /**
371                  * Creates a new fetched URI.
372                  *
373                  * @param freenetUri
374                  *            The URI that was fetched
375                  * @param fetchResult
376                  *            The fetch result
377                  */
378                 public Fetched(FreenetURI freenetUri, FetchResult fetchResult) {
379                         this.freenetUri = freenetUri;
380                         this.fetchResult = fetchResult;
381                 }
382
383                 //
384                 // ACCESSORS
385                 //
386
387                 /**
388                  * Returns the fetched URI.
389                  *
390                  * @return The fetched URI
391                  */
392                 public FreenetURI getFreenetUri() {
393                         return freenetUri;
394                 }
395
396                 /**
397                  * Returns the fetch result.
398                  *
399                  * @return The fetch result
400                  */
401                 public FetchResult getFetchResult() {
402                         return fetchResult;
403                 }
404
405         }
406
407         /**
408          * Callback for USK watcher events.
409          *
410          * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
411          */
412         public static interface Callback {
413
414                 /**
415                  * Notifies a listener that a new edition was found for a URI.
416                  *
417                  * @param uri
418                  *            The URI that a new edition was found for
419                  * @param edition
420                  *            The found edition
421                  * @param newKnownGood
422                  *            Whether the found edition was actually fetched
423                  * @param newSlot
424                  *            Whether the found edition is higher than all previously
425                  *            found editions
426                  */
427                 public void editionFound(FreenetURI uri, long edition, boolean newKnownGood, boolean newSlot);
428
429         }
430
431         /**
432          * Insert token that can cancel a running insert and sends events.
433          *
434          * @see ImageInsertAbortedEvent
435          * @see ImageInsertStartedEvent
436          * @see ImageInsertFailedEvent
437          * @see ImageInsertFinishedEvent
438          * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
439          */
440         public class InsertToken implements ClientPutCallback {
441
442                 /** The image being inserted. */
443                 private final Image image;
444
445                 /** The client putter. */
446                 private ClientPutter clientPutter;
447                 private Bucket bucket;
448
449                 /** The final URI. */
450                 private volatile FreenetURI resultingUri;
451
452                 /**
453                  * Creates a new insert token for the given image.
454                  *
455                  * @param image
456                  *            The image being inserted
457                  */
458                 public InsertToken(Image image) {
459                         this.image = image;
460                 }
461
462                 //
463                 // ACCESSORS
464                 //
465
466                 /**
467                  * Sets the client putter that is inserting the image. This will also
468                  * signal all registered listeners that the image has started.
469                  *
470                  * @param clientPutter
471                  *            The client putter
472                  */
473                 @SuppressWarnings("synthetic-access")
474                 public void setClientPutter(ClientPutter clientPutter) {
475                         this.clientPutter = clientPutter;
476                         eventBus.post(new ImageInsertStartedEvent(image));
477                 }
478
479                 public void setBucket(Bucket bucket) {
480                         this.bucket = bucket;
481                 }
482
483                 //
484                 // ACTIONS
485                 //
486
487                 /**
488                  * Cancels the running insert.
489                  */
490                 @SuppressWarnings("synthetic-access")
491                 public void cancel() {
492                         clientPutter.cancel(node.clientCore.clientContext);
493                         eventBus.post(new ImageInsertAbortedEvent(image));
494                         bucket.free();
495                 }
496
497                 //
498                 // INTERFACE ClientPutCallback
499                 //
500
501                 @Override
502                 public RequestClient getRequestClient() {
503                         return imageInserts;
504                 }
505
506                 @Override
507                 public void onResume(ClientContext context) throws ResumeFailedException {
508                         /* ignore. */
509                 }
510
511                 /**
512                  * {@inheritDoc}
513                  */
514                 @Override
515                 @SuppressWarnings("synthetic-access")
516                 public void onFailure(InsertException insertException, BaseClientPutter clientPutter) {
517                         if ((insertException != null) && ("Cancelled by user".equals(insertException.getMessage()))) {
518                                 eventBus.post(new ImageInsertAbortedEvent(image));
519                         } else {
520                                 eventBus.post(new ImageInsertFailedEvent(image, insertException));
521                         }
522                         bucket.free();
523                 }
524
525                 /**
526                  * {@inheritDoc}
527                  */
528                 @Override
529                 public void onFetchable(BaseClientPutter clientPutter) {
530                         /* ignore, we don’t care. */
531                 }
532
533                 /**
534                  * {@inheritDoc}
535                  */
536                 @Override
537                 public void onGeneratedMetadata(Bucket metadata, BaseClientPutter clientPutter) {
538                         /* ignore, we don’t care. */
539                 }
540
541                 /**
542                  * {@inheritDoc}
543                  */
544                 @Override
545                 public void onGeneratedURI(FreenetURI generatedUri, BaseClientPutter clientPutter) {
546                         resultingUri = generatedUri;
547                 }
548
549                 /**
550                  * {@inheritDoc}
551                  */
552                 @Override
553                 @SuppressWarnings("synthetic-access")
554                 public void onSuccess(BaseClientPutter clientPutter) {
555                         eventBus.post(new ImageInsertFinishedEvent(image, resultingUri));
556                         bucket.free();
557                 }
558
559         }
560
561         public class InsertTokenSupplier implements Function<Image, InsertToken> {
562
563                 @Override
564                 public InsertToken apply(Image image) {
565                         return new InsertToken(image);
566                 }
567
568         }
569
570 }