Move Fetched class to top-level
[Sone.git] / src / main / java / net / pterodactylus / sone / core / SoneDownloaderImpl.java
1 /*
2  * Sone - SoneDownloaderImpl.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.support.io.Closer.close;
21 import static java.lang.String.format;
22 import static java.lang.System.currentTimeMillis;
23 import static java.util.concurrent.TimeUnit.DAYS;
24 import static java.util.logging.Logger.getLogger;
25
26 import java.io.InputStream;
27 import java.util.HashSet;
28 import java.util.Set;
29 import java.util.logging.Level;
30 import java.util.logging.Logger;
31
32 import javax.inject.Inject;
33
34 import net.pterodactylus.sone.data.Sone;
35 import net.pterodactylus.sone.data.Sone.SoneStatus;
36 import net.pterodactylus.util.service.AbstractService;
37
38 import freenet.client.FetchResult;
39 import freenet.client.async.ClientContext;
40 import freenet.client.async.USKCallback;
41 import freenet.keys.FreenetURI;
42 import freenet.keys.USK;
43 import freenet.node.RequestStarter;
44 import freenet.support.api.Bucket;
45
46 /**
47  * The Sone downloader is responsible for download Sones as they are updated.
48  *
49  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
50  */
51 public class SoneDownloaderImpl extends AbstractService implements SoneDownloader {
52
53         /** The logger. */
54         private static final Logger logger = getLogger(SoneDownloaderImpl.class.getName());
55
56         /** The maximum protocol version. */
57         private static final int MAX_PROTOCOL_VERSION = 0;
58
59         /** The core. */
60         private final Core core;
61         private final SoneParser soneParser;
62
63         /** The Freenet interface. */
64         private final FreenetInterface freenetInterface;
65
66         /** The sones to update. */
67         private final Set<Sone> sones = new HashSet<Sone>();
68
69         /**
70          * Creates a new Sone downloader.
71          *
72          * @param core
73          *              The core
74          * @param freenetInterface
75          *              The Freenet interface
76          * @param soneParser
77          */
78         @Inject
79         SoneDownloaderImpl(Core core, FreenetInterface freenetInterface, SoneParser soneParser) {
80                 super("Sone Downloader", false);
81                 this.core = core;
82                 this.freenetInterface = freenetInterface;
83                 this.soneParser = soneParser;
84         }
85
86         //
87         // ACTIONS
88         //
89
90         /**
91          * Adds the given Sone to the set of Sones that will be watched for updates.
92          *
93          * @param sone
94          *              The Sone to add
95          */
96         @Override
97         public void addSone(final Sone sone) {
98                 if (!sones.add(sone)) {
99                         freenetInterface.unregisterUsk(sone);
100                 }
101                 final USKCallback uskCallback = new USKCallback() {
102
103                         @Override
104                         @SuppressWarnings("synthetic-access")
105                         public void onFoundEdition(long edition, USK key,
106                                         ClientContext clientContext, boolean metadata,
107                                         short codec, byte[] data, boolean newKnownGood,
108                                         boolean newSlotToo) {
109                                 logger.log(Level.FINE, format(
110                                                 "Found USK update for Sone “%s” at %s, new known good: %s, new slot too: %s.",
111                                                 sone, key, newKnownGood, newSlotToo));
112                                 if (edition > sone.getLatestEdition()) {
113                                         sone.setLatestEdition(edition);
114                                         new Thread(fetchSoneAsSskAction(sone),
115                                                         "Sone Downloader").start();
116                                 }
117                         }
118
119                         @Override
120                         public short getPollingPriorityProgress() {
121                                 return RequestStarter.INTERACTIVE_PRIORITY_CLASS;
122                         }
123
124                         @Override
125                         public short getPollingPriorityNormal() {
126                                 return RequestStarter.INTERACTIVE_PRIORITY_CLASS;
127                         }
128                 };
129                 if (soneHasBeenActiveRecently(sone)) {
130                         freenetInterface.registerActiveUsk(sone.getRequestUri(),
131                                         uskCallback);
132                 } else {
133                         freenetInterface.registerPassiveUsk(sone.getRequestUri(),
134                                         uskCallback);
135                 }
136         }
137
138         private boolean soneHasBeenActiveRecently(Sone sone) {
139                 return (currentTimeMillis() - sone.getTime()) < DAYS.toMillis(7);
140         }
141
142         private void fetchSoneAsSsk(Sone sone) {
143                 fetchSone(sone, sone.getRequestUri().sskForUSK(), false);
144         }
145
146         /**
147          * Fetches the Sone from the given URI.
148          *
149          * @param sone
150          *              The Sone to fetch
151          * @param soneUri
152          *              The URI of the Sone to fetch
153          * @param fetchOnly
154          *              {@code true} to only fetch and parse the Sone, {@code false}
155          *              to {@link Core#updateSone(Sone) update} it in the core
156          * @return The downloaded Sone, or {@code null} if the Sone could not be
157          *         downloaded
158          */
159         @Override
160         public Sone fetchSone(Sone sone, FreenetURI soneUri, boolean fetchOnly) {
161                 logger.log(Level.FINE, String.format("Starting fetch for Sone “%s” from %s…", sone, soneUri));
162                 FreenetURI requestUri = soneUri.setMetaString(new String[] { "sone.xml" });
163                 sone.setStatus(SoneStatus.downloading);
164                 try {
165                         Fetched fetchResults = freenetInterface.fetchUri(requestUri);
166                         if (fetchResults == null) {
167                                 /* TODO - mark Sone as bad. */
168                                 return null;
169                         }
170                         logger.log(Level.FINEST, String.format("Got %d bytes back.", fetchResults.getFetchResult().size()));
171                         Sone parsedSone = parseSone(sone, fetchResults.getFetchResult(), fetchResults.getFreenetUri());
172                         if (parsedSone != null) {
173                                 if (!fetchOnly) {
174                                         parsedSone.setStatus((parsedSone.getTime() == 0) ? SoneStatus.unknown : SoneStatus.idle);
175                                         core.updateSone(parsedSone);
176                                         addSone(parsedSone);
177                                 }
178                         }
179                         return parsedSone;
180                 } finally {
181                         sone.setStatus((sone.getTime() == 0) ? SoneStatus.unknown : SoneStatus.idle);
182                 }
183         }
184
185         /**
186          * Parses a Sone from a fetch result.
187          *
188          * @param originalSone
189          *              The sone to parse, or {@code null} if the Sone is yet unknown
190          * @param fetchResult
191          *              The fetch result
192          * @param requestUri
193          *              The requested URI
194          * @return The parsed Sone, or {@code null} if the Sone could not be parsed
195          */
196         private Sone parseSone(Sone originalSone, FetchResult fetchResult, FreenetURI requestUri) {
197                 logger.log(Level.FINEST, String.format("Parsing FetchResult (%d bytes, %s) for %s…", fetchResult.size(), fetchResult.getMimeType(), originalSone));
198                 Bucket soneBucket = fetchResult.asBucket();
199                 InputStream soneInputStream = null;
200                 try {
201                         soneInputStream = soneBucket.getInputStream();
202                         Sone parsedSone = soneParser.parseSone(originalSone,
203                                         soneInputStream);
204                         if (parsedSone != null) {
205                                 parsedSone.setLatestEdition(requestUri.getEdition());
206                         }
207                         return parsedSone;
208                 } catch (Exception e1) {
209                         logger.log(Level.WARNING, String.format("Could not parse Sone from %s!", requestUri), e1);
210                 } finally {
211                         close(soneInputStream);
212                         close(soneBucket);
213                 }
214                 return null;
215         }
216
217         @Override
218         public Runnable fetchSoneAsUskAction(final Sone sone) {
219                 return new Runnable() {
220                         @Override
221                         public void run() {
222                                 fetchSone(sone, sone.getRequestUri(), false);
223                         }
224                 };
225         }
226
227         @Override
228         public Runnable fetchSoneAsSskAction(final Sone sone) {
229                 return new Runnable() {
230                         @Override
231                         public void run() {
232                                 fetchSoneAsSsk(sone);
233                         }
234                 };
235         }
236
237         /** {@inheritDoc} */
238         @Override
239         protected void serviceStop() {
240                 for (Sone sone : sones) {
241                         freenetInterface.unregisterUsk(sone);
242                 }
243         }
244
245 }