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