Move Sone parser into its own class.
[Sone.git] / src / main / java / net / pterodactylus / sone / core / SoneParser.java
1 package net.pterodactylus.sone.core;
2
3 import java.io.InputStream;
4 import java.util.ArrayList;
5 import java.util.HashMap;
6 import java.util.HashSet;
7 import java.util.List;
8 import java.util.Map;
9 import java.util.Set;
10 import java.util.logging.Level;
11 import java.util.logging.Logger;
12
13 import net.pterodactylus.sone.data.Album;
14 import net.pterodactylus.sone.data.Client;
15 import net.pterodactylus.sone.data.Image;
16 import net.pterodactylus.sone.data.Post;
17 import net.pterodactylus.sone.data.PostReply;
18 import net.pterodactylus.sone.data.Profile;
19 import net.pterodactylus.sone.data.Sone;
20 import net.pterodactylus.sone.database.PostBuilder;
21 import net.pterodactylus.sone.database.PostReplyBuilder;
22 import net.pterodactylus.sone.database.SoneBuilder;
23 import net.pterodactylus.util.logging.Logging;
24 import net.pterodactylus.util.number.Numbers;
25 import net.pterodactylus.util.xml.SimpleXML;
26 import net.pterodactylus.util.xml.XML;
27
28 import org.w3c.dom.Document;
29
30 /**
31  * Parses a {@link Sone} from an XML {@link InputStream}.
32  *
33  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
34  */
35 public class SoneParser {
36
37         private static final Logger logger = Logging.getLogger(SoneParser.class);
38         private static final int MAX_PROTOCOL_VERSION = 0;
39         private final Core core;
40
41         public SoneParser(Core core) {
42                 this.core = core;
43         }
44
45         public Sone parseSone(Sone originalSone, InputStream soneInputStream) throws SoneException {
46                 /* TODO - impose a size limit? */
47
48                 Document document;
49                 /* XML parsing is not thread-safe. */
50                 synchronized (this) {
51                         document = XML.transformToDocument(soneInputStream);
52                 }
53                 if (document == null) {
54                         /* TODO - mark Sone as bad. */
55                         logger.log(Level.WARNING, String.format("Could not parse XML for Sone %s!", originalSone));
56                         return null;
57                 }
58
59                 SoneBuilder soneBuilder = core.soneBuilder().from(originalSone.getIdentity());
60                 if (originalSone.isLocal()) {
61                         soneBuilder = soneBuilder.local();
62                 }
63                 Sone sone = soneBuilder.build();
64
65                 SimpleXML soneXml;
66                 try {
67                         soneXml = SimpleXML.fromDocument(document);
68                 } catch (NullPointerException npe1) {
69                         /* for some reason, invalid XML can cause NPEs. */
70                         logger.log(Level.WARNING, String.format("XML for Sone %s can not be parsed!", sone), npe1);
71                         return null;
72                 }
73
74                 Integer protocolVersion = null;
75                 String soneProtocolVersion = soneXml.getValue("protocol-version", null);
76                 if (soneProtocolVersion != null) {
77                         protocolVersion = Numbers.safeParseInteger(soneProtocolVersion);
78                 }
79                 if (protocolVersion == null) {
80                         logger.log(Level.INFO, "No protocol version found, assuming 0.");
81                         protocolVersion = 0;
82                 }
83
84                 if (protocolVersion < 0) {
85                         logger.log(Level.WARNING, String.format("Invalid protocol version: %d! Not parsing Sone.", protocolVersion));
86                         return null;
87                 }
88
89                 /* check for valid versions. */
90                 if (protocolVersion > MAX_PROTOCOL_VERSION) {
91                         logger.log(Level.WARNING, String.format("Unknown protocol version: %d! Not parsing Sone.", protocolVersion));
92                         return null;
93                 }
94
95                 String soneTime = soneXml.getValue("time", null);
96                 if (soneTime == null) {
97                         /* TODO - mark Sone as bad. */
98                         logger.log(Level.WARNING, String.format("Downloaded time for Sone %s was null!", sone));
99                         return null;
100                 }
101                 try {
102                         sone.setTime(Long.parseLong(soneTime));
103                 } catch (NumberFormatException nfe1) {
104                         /* TODO - mark Sone as bad. */
105                         logger.log(Level.WARNING, String.format("Downloaded Sone %s with invalid time: %s", sone, soneTime));
106                         return null;
107                 }
108
109                 SimpleXML clientXml = soneXml.getNode("client");
110                 if (clientXml != null) {
111                         String clientName = clientXml.getValue("name", null);
112                         String clientVersion = clientXml.getValue("version", null);
113                         if ((clientName == null) || (clientVersion == null)) {
114                                 logger.log(Level.WARNING, String.format("Download Sone %s with client XML but missing name or version!", sone));
115                                 return null;
116                         }
117                         sone.setClient(new Client(clientName, clientVersion));
118                 }
119
120                 SimpleXML profileXml = soneXml.getNode("profile");
121                 if (profileXml == null) {
122                         /* TODO - mark Sone as bad. */
123                         logger.log(Level.WARNING, String.format("Downloaded Sone %s has no profile!", sone));
124                         return null;
125                 }
126
127                 /* parse profile. */
128                 String profileFirstName = profileXml.getValue("first-name", null);
129                 String profileMiddleName = profileXml.getValue("middle-name", null);
130                 String profileLastName = profileXml.getValue("last-name", null);
131                 Integer profileBirthDay = Numbers.safeParseInteger(profileXml.getValue("birth-day", null));
132                 Integer profileBirthMonth = Numbers.safeParseInteger(profileXml.getValue("birth-month", null));
133                 Integer profileBirthYear = Numbers.safeParseInteger(profileXml.getValue("birth-year", null));
134                 Profile profile = new Profile(sone).setFirstName(profileFirstName).setMiddleName(profileMiddleName).setLastName(profileLastName);
135                 profile.setBirthDay(profileBirthDay).setBirthMonth(profileBirthMonth).setBirthYear(profileBirthYear);
136                 /* avatar is processed after images are loaded. */
137                 String avatarId = profileXml.getValue("avatar", null);
138
139                 /* parse profile fields. */
140                 SimpleXML profileFieldsXml = profileXml.getNode("fields");
141                 if (profileFieldsXml != null) {
142                         for (SimpleXML fieldXml : profileFieldsXml.getNodes("field")) {
143                                 String fieldName = fieldXml.getValue("field-name", null);
144                                 String fieldValue = fieldXml.getValue("field-value", "");
145                                 if (fieldName == null) {
146                                         logger.log(Level.WARNING, String.format("Downloaded profile field for Sone %s with missing data! Name: %s, Value: %s", sone, fieldName, fieldValue));
147                                         return null;
148                                 }
149                                 try {
150                                         profile.addField(fieldName.trim()).setValue(fieldValue);
151                                 } catch (IllegalArgumentException iae1) {
152                                         logger.log(Level.WARNING, String.format("Duplicate field: %s", fieldName), iae1);
153                                         return null;
154                                 }
155                         }
156                 }
157
158                 /* parse posts. */
159                 SimpleXML postsXml = soneXml.getNode("posts");
160                 Set<Post> posts = new HashSet<Post>();
161                 if (postsXml == null) {
162                         /* TODO - mark Sone as bad. */
163                         logger.log(Level.WARNING, String.format("Downloaded Sone %s has no posts!", sone));
164                 } else {
165                         for (SimpleXML postXml : postsXml.getNodes("post")) {
166                                 String postId = postXml.getValue("id", null);
167                                 String postRecipientId = postXml.getValue("recipient", null);
168                                 String postTime = postXml.getValue("time", null);
169                                 String postText = postXml.getValue("text", null);
170                                 if ((postId == null) || (postTime == null) || (postText == null)) {
171                                         /* TODO - mark Sone as bad. */
172                                         logger.log(Level.WARNING, String.format("Downloaded post for Sone %s with missing data! ID: %s, Time: %s, Text: %s", sone, postId, postTime, postText));
173                                         return null;
174                                 }
175                                 try {
176                                         PostBuilder postBuilder = core.postBuilder();
177                                         /* TODO - parse time correctly. */
178                                         postBuilder.withId(postId).from(sone.getId()).withTime(Long.parseLong(postTime)).withText(postText);
179                                         if ((postRecipientId != null) && (postRecipientId.length() == 43)) {
180                                                 postBuilder.to(postRecipientId);
181                                         }
182                                         posts.add(postBuilder.build());
183                                 } catch (NumberFormatException nfe1) {
184                                         /* TODO - mark Sone as bad. */
185                                         logger.log(Level.WARNING, String.format("Downloaded post for Sone %s with invalid time: %s", sone, postTime));
186                                         return null;
187                                 }
188                         }
189                 }
190
191                 /* parse replies. */
192                 SimpleXML repliesXml = soneXml.getNode("replies");
193                 Set<PostReply> replies = new HashSet<PostReply>();
194                 if (repliesXml == null) {
195                         /* TODO - mark Sone as bad. */
196                         logger.log(Level.WARNING, String.format("Downloaded Sone %s has no replies!", sone));
197                 } else {
198                         for (SimpleXML replyXml : repliesXml.getNodes("reply")) {
199                                 String replyId = replyXml.getValue("id", null);
200                                 String replyPostId = replyXml.getValue("post-id", null);
201                                 String replyTime = replyXml.getValue("time", null);
202                                 String replyText = replyXml.getValue("text", null);
203                                 if ((replyId == null) || (replyPostId == null) || (replyTime == null) || (replyText == null)) {
204                                         /* TODO - mark Sone as bad. */
205                                         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));
206                                         return null;
207                                 }
208                                 try {
209                                         PostReplyBuilder postReplyBuilder = core.postReplyBuilder();
210                                         /* TODO - parse time correctly. */
211                                         postReplyBuilder.withId(replyId).from(sone.getId()).to(replyPostId).withTime(Long.parseLong(replyTime)).withText(replyText);
212                                         replies.add(postReplyBuilder.build());
213                                 } catch (NumberFormatException nfe1) {
214                                         /* TODO - mark Sone as bad. */
215                                         logger.log(Level.WARNING, String.format("Downloaded reply for Sone %s with invalid time: %s", sone, replyTime));
216                                         return null;
217                                 }
218                         }
219                 }
220
221                 /* parse liked post IDs. */
222                 SimpleXML likePostIdsXml = soneXml.getNode("post-likes");
223                 Set<String> likedPostIds = new HashSet<String>();
224                 if (likePostIdsXml == null) {
225                         /* TODO - mark Sone as bad. */
226                         logger.log(Level.WARNING, String.format("Downloaded Sone %s has no post likes!", sone));
227                 } else {
228                         for (SimpleXML likedPostIdXml : likePostIdsXml.getNodes("post-like")) {
229                                 String postId = likedPostIdXml.getValue();
230                                 likedPostIds.add(postId);
231                         }
232                 }
233
234                 /* parse liked reply IDs. */
235                 SimpleXML likeReplyIdsXml = soneXml.getNode("reply-likes");
236                 Set<String> likedReplyIds = new HashSet<String>();
237                 if (likeReplyIdsXml == null) {
238                         /* TODO - mark Sone as bad. */
239                         logger.log(Level.WARNING, String.format("Downloaded Sone %s has no reply likes!", sone));
240                 } else {
241                         for (SimpleXML likedReplyIdXml : likeReplyIdsXml.getNodes("reply-like")) {
242                                 String replyId = likedReplyIdXml.getValue();
243                                 likedReplyIds.add(replyId);
244                         }
245                 }
246
247                 /* parse albums. */
248                 SimpleXML albumsXml = soneXml.getNode("albums");
249                 Map<String, Image> allImages = new HashMap<String, Image>();
250                 List<Album> topLevelAlbums = new ArrayList<Album>();
251                 if (albumsXml != null) {
252                         for (SimpleXML albumXml : albumsXml.getNodes("album")) {
253                                 String id = albumXml.getValue("id", null);
254                                 String parentId = albumXml.getValue("parent", null);
255                                 String title = albumXml.getValue("title", null);
256                                 String description = albumXml.getValue("description", "");
257                                 String albumImageId = albumXml.getValue("album-image", null);
258                                 if ((id == null) || (title == null)) {
259                                         logger.log(Level.WARNING, String.format("Downloaded Sone %s contains invalid album!", sone));
260                                         return null;
261                                 }
262                                 Album parent = null;
263                                 if (parentId != null) {
264                                         parent = core.getAlbum(parentId);
265                                         if (parent == null) {
266                                                 logger.log(Level.WARNING, String.format("Downloaded Sone %s has album with invalid parent!", sone));
267                                                 return null;
268                                         }
269                                 }
270                                 Album album = core.albumBuilder()
271                                                 .withId(id)
272                                                 .by(sone)
273                                                 .build()
274                                                 .modify()
275                                                 .setTitle(title)
276                                                 .setDescription(description)
277                                                 .update();
278                                 if (parent != null) {
279                                         parent.addAlbum(album);
280                                 } else {
281                                         topLevelAlbums.add(album);
282                                 }
283                                 SimpleXML imagesXml = albumXml.getNode("images");
284                                 if (imagesXml != null) {
285                                         for (SimpleXML imageXml : imagesXml.getNodes("image")) {
286                                                 String imageId = imageXml.getValue("id", null);
287                                                 String imageCreationTimeString = imageXml.getValue("creation-time", null);
288                                                 String imageKey = imageXml.getValue("key", null);
289                                                 String imageTitle = imageXml.getValue("title", null);
290                                                 String imageDescription = imageXml.getValue("description", "");
291                                                 String imageWidthString = imageXml.getValue("width", null);
292                                                 String imageHeightString = imageXml.getValue("height", null);
293                                                 if ((imageId == null) || (imageCreationTimeString == null) || (imageKey == null) || (imageTitle == null) || (imageWidthString == null) || (imageHeightString == null)) {
294                                                         logger.log(Level.WARNING, String.format("Downloaded Sone %s contains invalid images!", sone));
295                                                         return null;
296                                                 }
297                                                 long creationTime = Numbers.safeParseLong(imageCreationTimeString, 0L);
298                                                 int imageWidth = Numbers.safeParseInteger(imageWidthString, 0);
299                                                 int imageHeight = Numbers.safeParseInteger(imageHeightString, 0);
300                                                 if ((imageWidth < 1) || (imageHeight < 1)) {
301                                                         logger.log(Level.WARNING, String.format("Downloaded Sone %s contains image %s with invalid dimensions (%s, %s)!", sone, imageId, imageWidthString, imageHeightString));
302                                                         return null;
303                                                 }
304                                                 Image image = core.imageBuilder().withId(imageId).build().modify().setSone(sone).setKey(imageKey).setCreationTime(creationTime).update();
305                                                 image = image.modify().setTitle(imageTitle).setDescription(imageDescription).update();
306                                                 image = image.modify().setWidth(imageWidth).setHeight(imageHeight).update();
307                                                 album.addImage(image);
308                                                 allImages.put(imageId, image);
309                                         }
310                                 }
311                                 album.modify().setAlbumImage(albumImageId).update();
312                         }
313                 }
314
315                 /* process avatar. */
316                 if (avatarId != null) {
317                         profile.setAvatar(allImages.get(avatarId));
318                 }
319
320                 /* okay, apparently everything was parsed correctly. Now import. */
321                 /* atomic setter operation on the Sone. */
322                 synchronized (sone) {
323                         sone.setProfile(profile);
324                         sone.setPosts(posts);
325                         sone.setReplies(replies);
326                         sone.setLikePostIds(likedPostIds);
327                         sone.setLikeReplyIds(likedReplyIds);
328                         for (Album album : topLevelAlbums) {
329                                 sone.getRootAlbum().addAlbum(album);
330                         }
331                 }
332
333                 return sone;
334
335         }
336
337 }