Merge branch 'last-working' into next
[Sone.git] / src / main / java / net / pterodactylus / sone / core / SoneDownloaderImpl.java
diff --git a/src/main/java/net/pterodactylus/sone/core/SoneDownloaderImpl.java b/src/main/java/net/pterodactylus/sone/core/SoneDownloaderImpl.java
new file mode 100644 (file)
index 0000000..b36c2d3
--- /dev/null
@@ -0,0 +1,274 @@
+/*
+ * Sone - SoneDownloader.java - Copyright © 2010–2013 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core;
+
+import static freenet.support.io.Closer.close;
+import static java.lang.String.format;
+import static java.lang.System.currentTimeMillis;
+import static java.util.concurrent.TimeUnit.DAYS;
+import static java.util.logging.Logger.getLogger;
+
+import java.io.InputStream;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import net.pterodactylus.sone.core.FreenetInterface.Fetched;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.data.Sone.SoneStatus;
+import net.pterodactylus.util.service.AbstractService;
+
+import freenet.client.FetchResult;
+import freenet.client.async.ClientContext;
+import freenet.client.async.USKCallback;
+import freenet.keys.FreenetURI;
+import freenet.keys.USK;
+import freenet.node.RequestStarter;
+import freenet.support.api.Bucket;
+import freenet.support.io.Closer;
+import com.db4o.ObjectContainer;
+
+import com.google.common.annotations.VisibleForTesting;
+
+/**
+ * The Sone downloader is responsible for download Sones as they are updated.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class SoneDownloaderImpl extends AbstractService implements SoneDownloader {
+
+       /** The logger. */
+       private static final Logger logger = getLogger("Sone.Downloader");
+
+       /** The maximum protocol version. */
+       private static final int MAX_PROTOCOL_VERSION = 0;
+
+       /** The core. */
+       private final Core core;
+       private final SoneParser soneParser;
+
+       /** The Freenet interface. */
+       private final FreenetInterface freenetInterface;
+
+       /** The sones to update. */
+       private final Set<Sone> sones = new HashSet<Sone>();
+
+       /**
+        * Creates a new Sone downloader.
+        *
+        * @param core
+        *              The core
+        * @param freenetInterface
+        *              The Freenet interface
+        */
+       public SoneDownloaderImpl(Core core, FreenetInterface freenetInterface) {
+               this(core, freenetInterface, new SoneParser(core));
+       }
+
+       /**
+        * Creates a new Sone downloader.
+        *
+        * @param core
+        *              The core
+        * @param freenetInterface
+        *              The Freenet interface
+        * @param soneParser
+        */
+       @VisibleForTesting
+       SoneDownloaderImpl(Core core, FreenetInterface freenetInterface, SoneParser soneParser) {
+               super("Sone Downloader", false);
+               this.core = core;
+               this.freenetInterface = freenetInterface;
+               this.soneParser = soneParser;
+       }
+
+       //
+       // ACTIONS
+       //
+
+       /**
+        * Adds the given Sone to the set of Sones that will be watched for updates.
+        *
+        * @param sone
+        *              The Sone to add
+        */
+       @Override
+       public void addSone(final Sone sone) {
+               if (!sones.add(sone)) {
+                       freenetInterface.unregisterUsk(sone);
+               }
+               final USKCallback uskCallback = new USKCallback() {
+
+                       @Override
+                       @SuppressWarnings("synthetic-access")
+                       public void onFoundEdition(long edition, USK key,
+                                       ClientContext clientContext, boolean metadata,
+                                       short codec, byte[] data, boolean newKnownGood,
+                                       boolean newSlotToo) {
+                               logger.log(Level.FINE, format(
+                                               "Found USK update for Sone “%s” at %s, new known good: %s, new slot too: %s.",
+                                               sone, key, newKnownGood, newSlotToo));
+                               if (edition > sone.getLatestEdition()) {
+                                       sone.setLatestEdition(edition);
+                                       new Thread(fetchSoneAction(sone),
+                                                       "Sone Downloader").start();
+                               }
+                       }
+
+                       @Override
+                       public short getPollingPriorityProgress() {
+                               return RequestStarter.INTERACTIVE_PRIORITY_CLASS;
+                       }
+
+                       @Override
+                       public short getPollingPriorityNormal() {
+                               return RequestStarter.INTERACTIVE_PRIORITY_CLASS;
+                       }
+               };
+               if (soneHasBeenActiveRecently(sone)) {
+                       freenetInterface.registerActiveUsk(sone.getRequestUri(),
+                                       uskCallback);
+               } else {
+                       freenetInterface.registerPassiveUsk(sone.getRequestUri(),
+                                       uskCallback);
+               }
+       }
+
+       private boolean soneHasBeenActiveRecently(Sone sone) {
+               return (currentTimeMillis() - sone.getTime()) < DAYS.toMillis(7);
+       }
+
+       private void fetchSone(Sone sone) {
+               fetchSone(sone, sone.getRequestUri().sskForUSK());
+       }
+
+       /**
+        * Fetches the updated Sone. This method can be used to fetch a Sone from a
+        * specific URI.
+        *
+        * @param sone
+        *              The Sone to fetch
+        * @param soneUri
+        *              The URI to fetch the Sone from
+        */
+       @Override
+       public void fetchSone(Sone sone, FreenetURI soneUri) {
+               fetchSone(sone, soneUri, false);
+       }
+
+       /**
+        * Fetches the Sone from the given URI.
+        *
+        * @param sone
+        *              The Sone to fetch
+        * @param soneUri
+        *              The URI of the Sone to fetch
+        * @param fetchOnly
+        *              {@code true} to only fetch and parse the Sone, {@code false}
+        *              to {@link Core#updateSone(Sone) update} it in the core
+        * @return The downloaded Sone, or {@code null} if the Sone could not be
+        *         downloaded
+        */
+       @Override
+       public Sone fetchSone(Sone sone, FreenetURI soneUri, boolean fetchOnly) {
+               logger.log(Level.FINE, String.format("Starting fetch for Sone “%s” from %s…", sone, soneUri));
+               FreenetURI requestUri = soneUri.setMetaString(new String[] { "sone.xml" });
+               sone.setStatus(SoneStatus.downloading);
+               try {
+                       Fetched fetchResults = freenetInterface.fetchUri(requestUri);
+                       if (fetchResults == null) {
+                               /* TODO - mark Sone as bad. */
+                               return null;
+                       }
+                       logger.log(Level.FINEST, String.format("Got %d bytes back.", fetchResults.getFetchResult().size()));
+                       Sone parsedSone = parseSone(sone, fetchResults.getFetchResult(), fetchResults.getFreenetUri());
+                       if (parsedSone != null) {
+                               if (!fetchOnly) {
+                                       parsedSone.setStatus((parsedSone.getTime() == 0) ? SoneStatus.unknown : SoneStatus.idle);
+                                       core.updateSone(parsedSone);
+                                       addSone(parsedSone);
+                               }
+                       }
+                       return parsedSone;
+               } finally {
+                       sone.setStatus((sone.getTime() == 0) ? SoneStatus.unknown : SoneStatus.idle);
+               }
+       }
+
+       /**
+        * Parses a Sone from a fetch result.
+        *
+        * @param originalSone
+        *              The sone to parse, or {@code null} if the Sone is yet unknown
+        * @param fetchResult
+        *              The fetch result
+        * @param requestUri
+        *              The requested URI
+        * @return The parsed Sone, or {@code null} if the Sone could not be parsed
+        */
+       private Sone parseSone(Sone originalSone, FetchResult fetchResult, FreenetURI requestUri) {
+               logger.log(Level.FINEST, String.format("Parsing FetchResult (%d bytes, %s) for %s…", fetchResult.size(), fetchResult.getMimeType(), originalSone));
+               Bucket soneBucket = fetchResult.asBucket();
+               InputStream soneInputStream = null;
+               try {
+                       soneInputStream = soneBucket.getInputStream();
+                       Sone parsedSone = soneParser.parseSone(originalSone,
+                                       soneInputStream);
+                       if (parsedSone != null) {
+                               parsedSone.setLatestEdition(requestUri.getEdition());
+                       }
+                       return parsedSone;
+               } catch (Exception e1) {
+                       logger.log(Level.WARNING, String.format("Could not parse Sone from %s!", requestUri), e1);
+               } finally {
+                       close(soneInputStream);
+                       close(soneBucket);
+               }
+               return null;
+       }
+
+       @Override
+       public Runnable fetchSoneWithUriAction(final Sone sone) {
+               return new Runnable() {
+                       @Override
+                       public void run() {
+                               fetchSone(sone, sone.getRequestUri());
+                       }
+               };
+       }
+
+       @Override
+       public Runnable fetchSoneAction(final Sone sone) {
+               return new Runnable() {
+                       @Override
+                       public void run() {
+                               fetchSone(sone);
+                       }
+               };
+       }
+
+       /** {@inheritDoc} */
+       @Override
+       protected void serviceStop() {
+               for (Sone sone : sones) {
+                       freenetInterface.unregisterUsk(sone);
+               }
+       }
+
+}