177b4626e4fad03b8a9446f202973b7dfe249436
[Sone.git] / src / main / java / net / pterodactylus / sone / core / SoneDownloader.java
1 /*
2  * Sone - SoneDownloader.java - Copyright © 2010 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 java.io.IOException;
21 import java.io.InputStream;
22 import java.net.MalformedURLException;
23 import java.util.HashSet;
24 import java.util.Set;
25 import java.util.logging.Level;
26 import java.util.logging.Logger;
27
28 import net.pterodactylus.sone.core.Core.Preferences;
29 import net.pterodactylus.sone.core.Core.SoneStatus;
30 import net.pterodactylus.sone.data.Client;
31 import net.pterodactylus.sone.data.Post;
32 import net.pterodactylus.sone.data.Profile;
33 import net.pterodactylus.sone.data.Reply;
34 import net.pterodactylus.sone.data.Sone;
35 import net.pterodactylus.util.collection.Pair;
36 import net.pterodactylus.util.io.Closer;
37 import net.pterodactylus.util.logging.Logging;
38 import net.pterodactylus.util.number.Numbers;
39 import net.pterodactylus.util.service.AbstractService;
40 import net.pterodactylus.util.xml.SimpleXML;
41 import net.pterodactylus.util.xml.XML;
42
43 import org.w3c.dom.Document;
44
45 import freenet.client.FetchResult;
46 import freenet.keys.FreenetURI;
47 import freenet.support.api.Bucket;
48
49 /**
50  * The Sone downloader is responsible for download Sones as they are updated.
51  *
52  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
53  */
54 public class SoneDownloader extends AbstractService {
55
56         /** The logger. */
57         private static final Logger logger = Logging.getLogger(SoneDownloader.class);
58
59         /** The maximum protocol version. */
60         private static final int MAX_PROTOCOL_VERSION = 0;
61
62         /** The core. */
63         private final Core core;
64
65         /** The Freenet interface. */
66         private final FreenetInterface freenetInterface;
67
68         /** The sones to update. */
69         private final Set<Sone> sones = new HashSet<Sone>();
70
71         /**
72          * Creates a new Sone downloader.
73          *
74          * @param core
75          *            The core
76          * @param freenetInterface
77          *            The Freenet interface
78          */
79         public SoneDownloader(Core core, FreenetInterface freenetInterface) {
80                 super("Sone Downloader", false);
81                 this.core = core;
82                 this.freenetInterface = freenetInterface;
83         }
84
85         //
86         // ACTIONS
87         //
88
89         /**
90          * Adds the given Sone to the set of Sones that will be watched for updates.
91          *
92          * @param sone
93          *            The Sone to add
94          */
95         public void addSone(Sone sone) {
96                 if (sones.add(sone)) {
97                         freenetInterface.unregisterUsk(sone);
98                         freenetInterface.registerUsk(sone, this);
99                 }
100         }
101
102         /**
103          * Removes the given Sone from the downloader.
104          *
105          * @param sone
106          *            The Sone to stop watching
107          */
108         public void removeSone(Sone sone) {
109                 if (sones.remove(sone)) {
110                         freenetInterface.unregisterUsk(sone);
111                 }
112         }
113
114         /**
115          * Fetches the updated Sone. This method is a callback method for
116          * {@link FreenetInterface#registerUsk(Sone, SoneDownloader)}.
117          *
118          * @param sone
119          *            The Sone to fetch
120          */
121         public void fetchSone(Sone sone) {
122                 fetchSone(sone, sone.getRequestUri());
123         }
124
125         /**
126          * Fetches the updated Sone. This method can be used to fetch a Sone from a
127          * specific URI (which happens when {@link Preferences#isSoneRescueMode()
128          * „Sone rescue mode“} is active).
129          *
130          * @param sone
131          *            The Sone to fetch
132          * @param soneUri
133          *            The URI to fetch the Sone from
134          */
135         public void fetchSone(Sone sone, FreenetURI soneUri) {
136                 if (core.getSoneStatus(sone) == SoneStatus.downloading) {
137                         return;
138                 }
139                 logger.log(Level.FINE, "Starting fetch for Sone “%s” from %s…", new Object[] { sone, soneUri });
140                 FreenetURI requestUri = soneUri.setMetaString(new String[] { "sone.xml" });
141                 core.setSoneStatus(sone, SoneStatus.downloading);
142                 try {
143                         Pair<FreenetURI, FetchResult> fetchResults = freenetInterface.fetchUri(requestUri);
144                         if (fetchResults == null) {
145                                 /* TODO - mark Sone as bad. */
146                                 return;
147                         }
148                         logger.log(Level.FINEST, "Got %d bytes back.", fetchResults.getRight().size());
149                         Sone parsedSone = parseSone(sone, fetchResults.getRight(), fetchResults.getLeft());
150                         if (parsedSone != null) {
151                                 core.updateSone(parsedSone);
152                         }
153                 } finally {
154                         core.setSoneStatus(sone, (sone.getTime() == 0) ? SoneStatus.unknown : SoneStatus.idle);
155                 }
156         }
157
158         /**
159          * Parses a Sone from a fetch result.
160          *
161          * @param originalSone
162          *            The sone to parse, or {@code null} if the Sone is yet unknown
163          * @param fetchResult
164          *            The fetch result
165          * @param requestUri
166          *            The requested URI
167          * @return The parsed Sone, or {@code null} if the Sone could not be parsed
168          */
169         public Sone parseSone(Sone originalSone, FetchResult fetchResult, FreenetURI requestUri) {
170                 logger.log(Level.FINEST, "Parsing FetchResult (%d bytes, %s) for %s…", new Object[] { fetchResult.size(), fetchResult.getMimeType(), originalSone });
171                 Bucket soneBucket = fetchResult.asBucket();
172                 InputStream soneInputStream = null;
173                 try {
174                         soneInputStream = soneBucket.getInputStream();
175                         Sone parsedSone = parseSone(originalSone, soneInputStream);
176                         if (parsedSone != null) {
177                                 parsedSone.setLatestEdition(requestUri.getEdition());
178                                 if (requestUri.getKeyType().equals("USK")) {
179                                         parsedSone.setRequestUri(requestUri.setMetaString(new String[0]));
180                                 } else {
181                                         parsedSone.setRequestUri(requestUri.setKeyType("USK").setDocName("Sone").setMetaString(new String[0]));
182                                 }
183                         }
184                         return parsedSone;
185                 } catch (IOException ioe1) {
186                         logger.log(Level.WARNING, "Could not parse Sone from " + requestUri + "!", ioe1);
187                 } finally {
188                         Closer.close(soneInputStream);
189                         soneBucket.free();
190                 }
191                 return null;
192         }
193
194         /**
195          * Parses a Sone from the given input stream and creates a new Sone from the
196          * parsed data.
197          *
198          * @param originalSone
199          *            The Sone to update
200          * @param soneInputStream
201          *            The input stream to parse the Sone from
202          * @return The parsed Sone
203          */
204         public Sone parseSone(Sone originalSone, InputStream soneInputStream) {
205                 /* TODO - impose a size limit? */
206
207                 Document document;
208                 /* XML parsing is not thread-safe. */
209                 synchronized (this) {
210                         document = XML.transformToDocument(soneInputStream);
211                 }
212                 if (document == null) {
213                         /* TODO - mark Sone as bad. */
214                         logger.log(Level.WARNING, "Could not parse XML for Sone %s!", new Object[] { originalSone });
215                         return null;
216                 }
217
218                 Sone sone = new Sone(originalSone.getId()).setIdentity(originalSone.getIdentity());
219
220                 SimpleXML soneXml;
221                 try {
222                         soneXml = SimpleXML.fromDocument(document);
223                 } catch (NullPointerException npe1) {
224                         /* for some reason, invalid XML can cause NPEs. */
225                         logger.log(Level.WARNING, "XML for Sone " + sone + " can not be parsed!", npe1);
226                         return null;
227                 }
228
229                 Integer protocolVersion = null;
230                 String soneProtocolVersion = soneXml.getValue("protocol-version", null);
231                 if (soneProtocolVersion != null) {
232                         protocolVersion = Numbers.safeParseInteger(soneProtocolVersion);
233                 }
234                 if (protocolVersion == null) {
235                         logger.log(Level.INFO, "No protocol version found, assuming 0.");
236                         protocolVersion = 0;
237                 }
238
239                 if (protocolVersion < 0) {
240                         logger.log(Level.WARNING, "Invalid protocol version: " + protocolVersion + "! Not parsing Sone.");
241                         return null;
242                 }
243
244                 /* check for valid versions. */
245                 if (protocolVersion > MAX_PROTOCOL_VERSION) {
246                         logger.log(Level.WARNING, "Unknown protocol version: " + protocolVersion + "! Not parsing Sone.");
247                         return null;
248                 }
249
250                 String soneTime = soneXml.getValue("time", null);
251                 if (soneTime == null) {
252                         /* TODO - mark Sone as bad. */
253                         logger.log(Level.WARNING, "Downloaded time for Sone %s was null!", new Object[] { sone });
254                         return null;
255                 }
256                 try {
257                         sone.setTime(Long.parseLong(soneTime));
258                 } catch (NumberFormatException nfe1) {
259                         /* TODO - mark Sone as bad. */
260                         logger.log(Level.WARNING, "Downloaded Sone %s with invalid time: %s", new Object[] { sone, soneTime });
261                         return null;
262                 }
263
264                 SimpleXML clientXml = soneXml.getNode("client");
265                 if (clientXml != null) {
266                         String clientName = clientXml.getValue("name", null);
267                         String clientVersion = clientXml.getValue("version", null);
268                         if ((clientName == null) || (clientVersion == null)) {
269                                 logger.log(Level.WARNING, "Download Sone %s with client XML but missing name or version!", sone);
270                                 return null;
271                         }
272                         sone.setClient(new Client(clientName, clientVersion));
273                 }
274
275                 String soneRequestUri = soneXml.getValue("request-uri", null);
276                 if (soneRequestUri != null) {
277                         try {
278                                 sone.setRequestUri(new FreenetURI(soneRequestUri));
279                         } catch (MalformedURLException mue1) {
280                                 /* TODO - mark Sone as bad. */
281                                 logger.log(Level.WARNING, "Downloaded Sone " + sone + " has invalid request URI: " + soneRequestUri, mue1);
282                                 return null;
283                         }
284                 }
285
286                 String soneInsertUri = soneXml.getValue("insert-uri", null);
287                 if ((soneInsertUri != null) && (sone.getInsertUri() == null)) {
288                         try {
289                                 sone.setInsertUri(new FreenetURI(soneInsertUri));
290                                 sone.setLatestEdition(Math.max(sone.getRequestUri().getEdition(), sone.getInsertUri().getEdition()));
291                         } catch (MalformedURLException mue1) {
292                                 /* TODO - mark Sone as bad. */
293                                 logger.log(Level.WARNING, "Downloaded Sone " + sone + " has invalid insert URI: " + soneInsertUri, mue1);
294                                 return null;
295                         }
296                 }
297
298                 SimpleXML profileXml = soneXml.getNode("profile");
299                 if (profileXml == null) {
300                         /* TODO - mark Sone as bad. */
301                         logger.log(Level.WARNING, "Downloaded Sone %s has no profile!", new Object[] { sone });
302                         return null;
303                 }
304
305                 /* parse profile. */
306                 String profileFirstName = profileXml.getValue("first-name", null);
307                 String profileMiddleName = profileXml.getValue("middle-name", null);
308                 String profileLastName = profileXml.getValue("last-name", null);
309                 Integer profileBirthDay = Numbers.safeParseInteger(profileXml.getValue("birth-day", null));
310                 Integer profileBirthMonth = Numbers.safeParseInteger(profileXml.getValue("birth-month", null));
311                 Integer profileBirthYear = Numbers.safeParseInteger(profileXml.getValue("birth-year", null));
312                 Profile profile = new Profile().setFirstName(profileFirstName).setMiddleName(profileMiddleName).setLastName(profileLastName);
313                 profile.setBirthDay(profileBirthDay).setBirthMonth(profileBirthMonth).setBirthYear(profileBirthYear);
314
315                 /* parse profile fields. */
316                 SimpleXML profileFieldsXml = profileXml.getNode("fields");
317                 if (profileFieldsXml != null) {
318                         for (SimpleXML fieldXml : profileFieldsXml.getNodes("field")) {
319                                 String fieldName = fieldXml.getValue("field-name", null);
320                                 String fieldValue = fieldXml.getValue("field-value", null);
321                                 if ((fieldName == null) || (fieldValue == null)) {
322                                         logger.log(Level.WARNING, "Downloaded profile field for Sone %s with missing data! Name: %s, Value: %s", new Object[] { sone, fieldName, fieldValue });
323                                         return null;
324                                 }
325                                 try {
326                                         profile.addField(fieldName).setValue(fieldValue);
327                                 } catch (IllegalArgumentException iae1) {
328                                         logger.log(Level.WARNING, "Duplicate field: " + fieldName, iae1);
329                                         return null;
330                                 }
331                         }
332                 }
333
334                 /* parse posts. */
335                 SimpleXML postsXml = soneXml.getNode("posts");
336                 Set<Post> posts = new HashSet<Post>();
337                 if (postsXml == null) {
338                         /* TODO - mark Sone as bad. */
339                         logger.log(Level.WARNING, "Downloaded Sone %s has no posts!", new Object[] { sone });
340                 } else {
341                         for (SimpleXML postXml : postsXml.getNodes("post")) {
342                                 String postId = postXml.getValue("id", null);
343                                 String postRecipientId = postXml.getValue("recipient", null);
344                                 String postTime = postXml.getValue("time", null);
345                                 String postText = postXml.getValue("text", null);
346                                 if ((postId == null) || (postTime == null) || (postText == null)) {
347                                         /* TODO - mark Sone as bad. */
348                                         logger.log(Level.WARNING, "Downloaded post for Sone %s with missing data! ID: %s, Time: %s, Text: %s", new Object[] { sone, postId, postTime, postText });
349                                         return null;
350                                 }
351                                 try {
352                                         Post post = core.getPost(postId).setSone(sone).setTime(Long.parseLong(postTime)).setText(postText);
353                                         if ((postRecipientId != null) && (postRecipientId.length() == 43)) {
354                                                 post.setRecipient(core.getSone(postRecipientId));
355                                         }
356                                         posts.add(post);
357                                 } catch (NumberFormatException nfe1) {
358                                         /* TODO - mark Sone as bad. */
359                                         logger.log(Level.WARNING, "Downloaded post for Sone %s with invalid time: %s", new Object[] { sone, postTime });
360                                         return null;
361                                 }
362                         }
363                 }
364
365                 /* parse replies. */
366                 SimpleXML repliesXml = soneXml.getNode("replies");
367                 Set<Reply> replies = new HashSet<Reply>();
368                 if (repliesXml == null) {
369                         /* TODO - mark Sone as bad. */
370                         logger.log(Level.WARNING, "Downloaded Sone %s has no replies!", new Object[] { sone });
371                 } else {
372                         for (SimpleXML replyXml : repliesXml.getNodes("reply")) {
373                                 String replyId = replyXml.getValue("id", null);
374                                 String replyPostId = replyXml.getValue("post-id", null);
375                                 String replyTime = replyXml.getValue("time", null);
376                                 String replyText = replyXml.getValue("text", null);
377                                 if ((replyId == null) || (replyPostId == null) || (replyTime == null) || (replyText == null)) {
378                                         /* TODO - mark Sone as bad. */
379                                         logger.log(Level.WARNING, "Downloaded reply for Sone %s with missing data! ID: %s, Post: %s, Time: %s, Text: %s", new Object[] { sone, replyId, replyPostId, replyTime, replyText });
380                                         return null;
381                                 }
382                                 try {
383                                         replies.add(core.getReply(replyId).setSone(sone).setPost(core.getPost(replyPostId)).setTime(Long.parseLong(replyTime)).setText(replyText));
384                                 } catch (NumberFormatException nfe1) {
385                                         /* TODO - mark Sone as bad. */
386                                         logger.log(Level.WARNING, "Downloaded reply for Sone %s with invalid time: %s", new Object[] { sone, replyTime });
387                                         return null;
388                                 }
389                         }
390                 }
391
392                 /* parse liked post IDs. */
393                 SimpleXML likePostIdsXml = soneXml.getNode("post-likes");
394                 Set<String> likedPostIds = new HashSet<String>();
395                 if (likePostIdsXml == null) {
396                         /* TODO - mark Sone as bad. */
397                         logger.log(Level.WARNING, "Downloaded Sone %s has no post likes!", new Object[] { sone });
398                 } else {
399                         for (SimpleXML likedPostIdXml : likePostIdsXml.getNodes("post-like")) {
400                                 String postId = likedPostIdXml.getValue();
401                                 likedPostIds.add(postId);
402                         }
403                 }
404
405                 /* parse liked reply IDs. */
406                 SimpleXML likeReplyIdsXml = soneXml.getNode("reply-likes");
407                 Set<String> likedReplyIds = new HashSet<String>();
408                 if (likeReplyIdsXml == null) {
409                         /* TODO - mark Sone as bad. */
410                         logger.log(Level.WARNING, "Downloaded Sone %s has no reply likes!", new Object[] { sone });
411                 } else {
412                         for (SimpleXML likedReplyIdXml : likeReplyIdsXml.getNodes("reply-like")) {
413                                 String replyId = likedReplyIdXml.getValue();
414                                 likedReplyIds.add(replyId);
415                         }
416                 }
417
418                 /* okay, apparently everything was parsed correctly. Now import. */
419                 /* atomic setter operation on the Sone. */
420                 synchronized (sone) {
421                         sone.setProfile(profile);
422                         sone.setPosts(posts);
423                         sone.setReplies(replies);
424                         sone.setLikePostIds(likedPostIds);
425                         sone.setLikeReplyIds(likedReplyIds);
426                 }
427
428                 return sone;
429         }
430
431         //
432         // SERVICE METHODS
433         //
434
435         /**
436          * {@inheritDoc}
437          */
438         @Override
439         protected void serviceStop() {
440                 for (Sone sone : sones) {
441                         freenetInterface.unregisterUsk(sone);
442                 }
443         }
444
445 }