From 808a37413dbcd2f8a543f26bef5f639fccba6f4f Mon Sep 17 00:00:00 2001 From: =?utf8?q?David=20=E2=80=98Bombe=E2=80=99=20Roden?= Date: Fri, 3 Oct 2014 11:00:32 +0200 Subject: [PATCH] Move Sone parser into its own class. --- .../sone/core/SoneDownloaderImpl.java | 344 +-------- .../net/pterodactylus/sone/core/SoneParser.java | 337 +++++++++ .../sone/core/SoneDownloaderTest.java | 775 +------------------ .../pterodactylus/sone/core/SoneParserTest.java | 832 +++++++++++++++++++++ 4 files changed, 1189 insertions(+), 1099 deletions(-) create mode 100644 src/main/java/net/pterodactylus/sone/core/SoneParser.java create mode 100644 src/test/java/net/pterodactylus/sone/core/SoneParserTest.java diff --git a/src/main/java/net/pterodactylus/sone/core/SoneDownloaderImpl.java b/src/main/java/net/pterodactylus/sone/core/SoneDownloaderImpl.java index d4ed927..3297a0d 100644 --- a/src/main/java/net/pterodactylus/sone/core/SoneDownloaderImpl.java +++ b/src/main/java/net/pterodactylus/sone/core/SoneDownloaderImpl.java @@ -22,36 +22,17 @@ import static java.lang.System.currentTimeMillis; import static java.util.concurrent.TimeUnit.DAYS; import java.io.InputStream; -import java.net.MalformedURLException; -import java.util.ArrayList; -import java.util.HashMap; import java.util.HashSet; -import java.util.List; -import java.util.Map; import java.util.Set; -import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; import net.pterodactylus.sone.core.FreenetInterface.Fetched; -import net.pterodactylus.sone.data.Album; -import net.pterodactylus.sone.data.Client; -import net.pterodactylus.sone.data.Image; -import net.pterodactylus.sone.data.Post; -import net.pterodactylus.sone.data.PostReply; -import net.pterodactylus.sone.data.Profile; import net.pterodactylus.sone.data.Sone; import net.pterodactylus.sone.data.Sone.SoneStatus; -import net.pterodactylus.sone.data.SoneImpl; -import net.pterodactylus.sone.database.PostBuilder; -import net.pterodactylus.sone.database.PostReplyBuilder; -import net.pterodactylus.sone.database.SoneBuilder; import net.pterodactylus.util.io.Closer; import net.pterodactylus.util.logging.Logging; -import net.pterodactylus.util.number.Numbers; import net.pterodactylus.util.service.AbstractService; -import net.pterodactylus.util.xml.SimpleXML; -import net.pterodactylus.util.xml.XML; import freenet.client.FetchResult; import freenet.client.async.ClientContext; @@ -60,11 +41,9 @@ import freenet.keys.FreenetURI; import freenet.keys.USK; import freenet.node.RequestStarter; import freenet.support.api.Bucket; - import com.db4o.ObjectContainer; import com.google.common.annotations.VisibleForTesting; -import org.w3c.dom.Document; /** * The Sone downloader is responsible for download Sones as they are updated. @@ -81,6 +60,7 @@ public class SoneDownloaderImpl extends AbstractService implements SoneDownloade /** The core. */ private final Core core; + private final SoneParser soneParser; /** The Freenet interface. */ private final FreenetInterface freenetInterface; @@ -97,9 +77,24 @@ public class SoneDownloaderImpl extends AbstractService implements SoneDownloade * 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; } // @@ -233,7 +228,8 @@ public class SoneDownloaderImpl extends AbstractService implements SoneDownloade InputStream soneInputStream = null; try { soneInputStream = soneBucket.getInputStream(); - Sone parsedSone = parseSone(originalSone, soneInputStream); + Sone parsedSone = soneParser.parseSone(originalSone, + soneInputStream); if (parsedSone != null) { parsedSone.setLatestEdition(requestUri.getEdition()); } @@ -247,310 +243,6 @@ public class SoneDownloaderImpl extends AbstractService implements SoneDownloade return null; } - /** - * Parses a Sone from the given input stream and creates a new Sone from the - * parsed data. - * - * @param originalSone - * The Sone to update - * @param soneInputStream - * The input stream to parse the Sone from - * @return The parsed Sone - * @throws SoneException - * if a parse error occurs, or the protocol is invalid - */ - @VisibleForTesting - protected Sone parseSone(Sone originalSone, InputStream soneInputStream) throws SoneException { - /* TODO - impose a size limit? */ - - Document document; - /* XML parsing is not thread-safe. */ - synchronized (this) { - document = XML.transformToDocument(soneInputStream); - } - if (document == null) { - /* TODO - mark Sone as bad. */ - logger.log(Level.WARNING, String.format("Could not parse XML for Sone %s!", originalSone)); - return null; - } - - SoneBuilder soneBuilder = core.soneBuilder().from(originalSone.getIdentity()); - if (originalSone.isLocal()) { - soneBuilder = soneBuilder.local(); - } - Sone sone = soneBuilder.build(); - - SimpleXML soneXml; - try { - soneXml = SimpleXML.fromDocument(document); - } catch (NullPointerException npe1) { - /* for some reason, invalid XML can cause NPEs. */ - logger.log(Level.WARNING, String.format("XML for Sone %s can not be parsed!", sone), npe1); - return null; - } - - Integer protocolVersion = null; - String soneProtocolVersion = soneXml.getValue("protocol-version", null); - if (soneProtocolVersion != null) { - protocolVersion = Numbers.safeParseInteger(soneProtocolVersion); - } - if (protocolVersion == null) { - logger.log(Level.INFO, "No protocol version found, assuming 0."); - protocolVersion = 0; - } - - if (protocolVersion < 0) { - logger.log(Level.WARNING, String.format("Invalid protocol version: %d! Not parsing Sone.", protocolVersion)); - return null; - } - - /* check for valid versions. */ - if (protocolVersion > MAX_PROTOCOL_VERSION) { - logger.log(Level.WARNING, String.format("Unknown protocol version: %d! Not parsing Sone.", protocolVersion)); - return null; - } - - String soneTime = soneXml.getValue("time", null); - if (soneTime == null) { - /* TODO - mark Sone as bad. */ - logger.log(Level.WARNING, String.format("Downloaded time for Sone %s was null!", sone)); - return null; - } - try { - sone.setTime(Long.parseLong(soneTime)); - } catch (NumberFormatException nfe1) { - /* TODO - mark Sone as bad. */ - logger.log(Level.WARNING, String.format("Downloaded Sone %s with invalid time: %s", sone, soneTime)); - return null; - } - - SimpleXML clientXml = soneXml.getNode("client"); - if (clientXml != null) { - String clientName = clientXml.getValue("name", null); - String clientVersion = clientXml.getValue("version", null); - if ((clientName == null) || (clientVersion == null)) { - logger.log(Level.WARNING, String.format("Download Sone %s with client XML but missing name or version!", sone)); - return null; - } - sone.setClient(new Client(clientName, clientVersion)); - } - - SimpleXML profileXml = soneXml.getNode("profile"); - if (profileXml == null) { - /* TODO - mark Sone as bad. */ - logger.log(Level.WARNING, String.format("Downloaded Sone %s has no profile!", sone)); - return null; - } - - /* parse profile. */ - String profileFirstName = profileXml.getValue("first-name", null); - String profileMiddleName = profileXml.getValue("middle-name", null); - String profileLastName = profileXml.getValue("last-name", null); - Integer profileBirthDay = Numbers.safeParseInteger(profileXml.getValue("birth-day", null)); - Integer profileBirthMonth = Numbers.safeParseInteger(profileXml.getValue("birth-month", null)); - Integer profileBirthYear = Numbers.safeParseInteger(profileXml.getValue("birth-year", null)); - Profile profile = new Profile(sone).setFirstName(profileFirstName).setMiddleName(profileMiddleName).setLastName(profileLastName); - profile.setBirthDay(profileBirthDay).setBirthMonth(profileBirthMonth).setBirthYear(profileBirthYear); - /* avatar is processed after images are loaded. */ - String avatarId = profileXml.getValue("avatar", null); - - /* parse profile fields. */ - SimpleXML profileFieldsXml = profileXml.getNode("fields"); - if (profileFieldsXml != null) { - for (SimpleXML fieldXml : profileFieldsXml.getNodes("field")) { - String fieldName = fieldXml.getValue("field-name", null); - String fieldValue = fieldXml.getValue("field-value", ""); - if (fieldName == null) { - logger.log(Level.WARNING, String.format("Downloaded profile field for Sone %s with missing data! Name: %s, Value: %s", sone, fieldName, fieldValue)); - return null; - } - try { - profile.addField(fieldName.trim()).setValue(fieldValue); - } catch (IllegalArgumentException iae1) { - logger.log(Level.WARNING, String.format("Duplicate field: %s", fieldName), iae1); - return null; - } - } - } - - /* parse posts. */ - SimpleXML postsXml = soneXml.getNode("posts"); - Set posts = new HashSet(); - if (postsXml == null) { - /* TODO - mark Sone as bad. */ - logger.log(Level.WARNING, String.format("Downloaded Sone %s has no posts!", sone)); - } else { - for (SimpleXML postXml : postsXml.getNodes("post")) { - String postId = postXml.getValue("id", null); - String postRecipientId = postXml.getValue("recipient", null); - String postTime = postXml.getValue("time", null); - String postText = postXml.getValue("text", null); - if ((postId == null) || (postTime == null) || (postText == null)) { - /* TODO - mark Sone as bad. */ - logger.log(Level.WARNING, String.format("Downloaded post for Sone %s with missing data! ID: %s, Time: %s, Text: %s", sone, postId, postTime, postText)); - return null; - } - try { - PostBuilder postBuilder = core.postBuilder(); - /* TODO - parse time correctly. */ - postBuilder.withId(postId).from(sone.getId()).withTime(Long.parseLong(postTime)).withText(postText); - if ((postRecipientId != null) && (postRecipientId.length() == 43)) { - postBuilder.to(postRecipientId); - } - posts.add(postBuilder.build()); - } catch (NumberFormatException nfe1) { - /* TODO - mark Sone as bad. */ - logger.log(Level.WARNING, String.format("Downloaded post for Sone %s with invalid time: %s", sone, postTime)); - return null; - } - } - } - - /* parse replies. */ - SimpleXML repliesXml = soneXml.getNode("replies"); - Set replies = new HashSet(); - if (repliesXml == null) { - /* TODO - mark Sone as bad. */ - logger.log(Level.WARNING, String.format("Downloaded Sone %s has no replies!", sone)); - } else { - for (SimpleXML replyXml : repliesXml.getNodes("reply")) { - String replyId = replyXml.getValue("id", null); - String replyPostId = replyXml.getValue("post-id", null); - String replyTime = replyXml.getValue("time", null); - String replyText = replyXml.getValue("text", null); - if ((replyId == null) || (replyPostId == null) || (replyTime == null) || (replyText == null)) { - /* TODO - mark Sone as bad. */ - 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)); - return null; - } - try { - PostReplyBuilder postReplyBuilder = core.postReplyBuilder(); - /* TODO - parse time correctly. */ - postReplyBuilder.withId(replyId).from(sone.getId()).to(replyPostId).withTime(Long.parseLong(replyTime)).withText(replyText); - replies.add(postReplyBuilder.build()); - } catch (NumberFormatException nfe1) { - /* TODO - mark Sone as bad. */ - logger.log(Level.WARNING, String.format("Downloaded reply for Sone %s with invalid time: %s", sone, replyTime)); - return null; - } - } - } - - /* parse liked post IDs. */ - SimpleXML likePostIdsXml = soneXml.getNode("post-likes"); - Set likedPostIds = new HashSet(); - if (likePostIdsXml == null) { - /* TODO - mark Sone as bad. */ - logger.log(Level.WARNING, String.format("Downloaded Sone %s has no post likes!", sone)); - } else { - for (SimpleXML likedPostIdXml : likePostIdsXml.getNodes("post-like")) { - String postId = likedPostIdXml.getValue(); - likedPostIds.add(postId); - } - } - - /* parse liked reply IDs. */ - SimpleXML likeReplyIdsXml = soneXml.getNode("reply-likes"); - Set likedReplyIds = new HashSet(); - if (likeReplyIdsXml == null) { - /* TODO - mark Sone as bad. */ - logger.log(Level.WARNING, String.format("Downloaded Sone %s has no reply likes!", sone)); - } else { - for (SimpleXML likedReplyIdXml : likeReplyIdsXml.getNodes("reply-like")) { - String replyId = likedReplyIdXml.getValue(); - likedReplyIds.add(replyId); - } - } - - /* parse albums. */ - SimpleXML albumsXml = soneXml.getNode("albums"); - Map allImages = new HashMap(); - List topLevelAlbums = new ArrayList(); - if (albumsXml != null) { - for (SimpleXML albumXml : albumsXml.getNodes("album")) { - String id = albumXml.getValue("id", null); - String parentId = albumXml.getValue("parent", null); - String title = albumXml.getValue("title", null); - String description = albumXml.getValue("description", ""); - String albumImageId = albumXml.getValue("album-image", null); - if ((id == null) || (title == null)) { - logger.log(Level.WARNING, String.format("Downloaded Sone %s contains invalid album!", sone)); - return null; - } - Album parent = null; - if (parentId != null) { - parent = core.getAlbum(parentId); - if (parent == null) { - logger.log(Level.WARNING, String.format("Downloaded Sone %s has album with invalid parent!", sone)); - return null; - } - } - Album album = core.albumBuilder() - .withId(id) - .by(sone) - .build() - .modify() - .setTitle(title) - .setDescription(description) - .update(); - if (parent != null) { - parent.addAlbum(album); - } else { - topLevelAlbums.add(album); - } - SimpleXML imagesXml = albumXml.getNode("images"); - if (imagesXml != null) { - for (SimpleXML imageXml : imagesXml.getNodes("image")) { - String imageId = imageXml.getValue("id", null); - String imageCreationTimeString = imageXml.getValue("creation-time", null); - String imageKey = imageXml.getValue("key", null); - String imageTitle = imageXml.getValue("title", null); - String imageDescription = imageXml.getValue("description", ""); - String imageWidthString = imageXml.getValue("width", null); - String imageHeightString = imageXml.getValue("height", null); - if ((imageId == null) || (imageCreationTimeString == null) || (imageKey == null) || (imageTitle == null) || (imageWidthString == null) || (imageHeightString == null)) { - logger.log(Level.WARNING, String.format("Downloaded Sone %s contains invalid images!", sone)); - return null; - } - long creationTime = Numbers.safeParseLong(imageCreationTimeString, 0L); - int imageWidth = Numbers.safeParseInteger(imageWidthString, 0); - int imageHeight = Numbers.safeParseInteger(imageHeightString, 0); - if ((imageWidth < 1) || (imageHeight < 1)) { - logger.log(Level.WARNING, String.format("Downloaded Sone %s contains image %s with invalid dimensions (%s, %s)!", sone, imageId, imageWidthString, imageHeightString)); - return null; - } - Image image = core.imageBuilder().withId(imageId).build().modify().setSone(sone).setKey(imageKey).setCreationTime(creationTime).update(); - image = image.modify().setTitle(imageTitle).setDescription(imageDescription).update(); - image = image.modify().setWidth(imageWidth).setHeight(imageHeight).update(); - album.addImage(image); - allImages.put(imageId, image); - } - } - album.modify().setAlbumImage(albumImageId).update(); - } - } - - /* process avatar. */ - if (avatarId != null) { - profile.setAvatar(allImages.get(avatarId)); - } - - /* okay, apparently everything was parsed correctly. Now import. */ - /* atomic setter operation on the Sone. */ - synchronized (sone) { - sone.setProfile(profile); - sone.setPosts(posts); - sone.setReplies(replies); - sone.setLikePostIds(likedPostIds); - sone.setLikeReplyIds(likedReplyIds); - for (Album album : topLevelAlbums) { - sone.getRootAlbum().addAlbum(album); - } - } - - return sone; - } - @Override public Runnable fetchSoneWithUriAction(final Sone sone) { return new Runnable() { diff --git a/src/main/java/net/pterodactylus/sone/core/SoneParser.java b/src/main/java/net/pterodactylus/sone/core/SoneParser.java new file mode 100644 index 0000000..5dca54c --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/core/SoneParser.java @@ -0,0 +1,337 @@ +package net.pterodactylus.sone.core; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +import net.pterodactylus.sone.data.Album; +import net.pterodactylus.sone.data.Client; +import net.pterodactylus.sone.data.Image; +import net.pterodactylus.sone.data.Post; +import net.pterodactylus.sone.data.PostReply; +import net.pterodactylus.sone.data.Profile; +import net.pterodactylus.sone.data.Sone; +import net.pterodactylus.sone.database.PostBuilder; +import net.pterodactylus.sone.database.PostReplyBuilder; +import net.pterodactylus.sone.database.SoneBuilder; +import net.pterodactylus.util.logging.Logging; +import net.pterodactylus.util.number.Numbers; +import net.pterodactylus.util.xml.SimpleXML; +import net.pterodactylus.util.xml.XML; + +import org.w3c.dom.Document; + +/** + * Parses a {@link Sone} from an XML {@link InputStream}. + * + * @author David ‘Bombe’ Roden + */ +public class SoneParser { + + private static final Logger logger = Logging.getLogger(SoneParser.class); + private static final int MAX_PROTOCOL_VERSION = 0; + private final Core core; + + public SoneParser(Core core) { + this.core = core; + } + + public Sone parseSone(Sone originalSone, InputStream soneInputStream) throws SoneException { + /* TODO - impose a size limit? */ + + Document document; + /* XML parsing is not thread-safe. */ + synchronized (this) { + document = XML.transformToDocument(soneInputStream); + } + if (document == null) { + /* TODO - mark Sone as bad. */ + logger.log(Level.WARNING, String.format("Could not parse XML for Sone %s!", originalSone)); + return null; + } + + SoneBuilder soneBuilder = core.soneBuilder().from(originalSone.getIdentity()); + if (originalSone.isLocal()) { + soneBuilder = soneBuilder.local(); + } + Sone sone = soneBuilder.build(); + + SimpleXML soneXml; + try { + soneXml = SimpleXML.fromDocument(document); + } catch (NullPointerException npe1) { + /* for some reason, invalid XML can cause NPEs. */ + logger.log(Level.WARNING, String.format("XML for Sone %s can not be parsed!", sone), npe1); + return null; + } + + Integer protocolVersion = null; + String soneProtocolVersion = soneXml.getValue("protocol-version", null); + if (soneProtocolVersion != null) { + protocolVersion = Numbers.safeParseInteger(soneProtocolVersion); + } + if (protocolVersion == null) { + logger.log(Level.INFO, "No protocol version found, assuming 0."); + protocolVersion = 0; + } + + if (protocolVersion < 0) { + logger.log(Level.WARNING, String.format("Invalid protocol version: %d! Not parsing Sone.", protocolVersion)); + return null; + } + + /* check for valid versions. */ + if (protocolVersion > MAX_PROTOCOL_VERSION) { + logger.log(Level.WARNING, String.format("Unknown protocol version: %d! Not parsing Sone.", protocolVersion)); + return null; + } + + String soneTime = soneXml.getValue("time", null); + if (soneTime == null) { + /* TODO - mark Sone as bad. */ + logger.log(Level.WARNING, String.format("Downloaded time for Sone %s was null!", sone)); + return null; + } + try { + sone.setTime(Long.parseLong(soneTime)); + } catch (NumberFormatException nfe1) { + /* TODO - mark Sone as bad. */ + logger.log(Level.WARNING, String.format("Downloaded Sone %s with invalid time: %s", sone, soneTime)); + return null; + } + + SimpleXML clientXml = soneXml.getNode("client"); + if (clientXml != null) { + String clientName = clientXml.getValue("name", null); + String clientVersion = clientXml.getValue("version", null); + if ((clientName == null) || (clientVersion == null)) { + logger.log(Level.WARNING, String.format("Download Sone %s with client XML but missing name or version!", sone)); + return null; + } + sone.setClient(new Client(clientName, clientVersion)); + } + + SimpleXML profileXml = soneXml.getNode("profile"); + if (profileXml == null) { + /* TODO - mark Sone as bad. */ + logger.log(Level.WARNING, String.format("Downloaded Sone %s has no profile!", sone)); + return null; + } + + /* parse profile. */ + String profileFirstName = profileXml.getValue("first-name", null); + String profileMiddleName = profileXml.getValue("middle-name", null); + String profileLastName = profileXml.getValue("last-name", null); + Integer profileBirthDay = Numbers.safeParseInteger(profileXml.getValue("birth-day", null)); + Integer profileBirthMonth = Numbers.safeParseInteger(profileXml.getValue("birth-month", null)); + Integer profileBirthYear = Numbers.safeParseInteger(profileXml.getValue("birth-year", null)); + Profile profile = new Profile(sone).setFirstName(profileFirstName).setMiddleName(profileMiddleName).setLastName(profileLastName); + profile.setBirthDay(profileBirthDay).setBirthMonth(profileBirthMonth).setBirthYear(profileBirthYear); + /* avatar is processed after images are loaded. */ + String avatarId = profileXml.getValue("avatar", null); + + /* parse profile fields. */ + SimpleXML profileFieldsXml = profileXml.getNode("fields"); + if (profileFieldsXml != null) { + for (SimpleXML fieldXml : profileFieldsXml.getNodes("field")) { + String fieldName = fieldXml.getValue("field-name", null); + String fieldValue = fieldXml.getValue("field-value", ""); + if (fieldName == null) { + logger.log(Level.WARNING, String.format("Downloaded profile field for Sone %s with missing data! Name: %s, Value: %s", sone, fieldName, fieldValue)); + return null; + } + try { + profile.addField(fieldName.trim()).setValue(fieldValue); + } catch (IllegalArgumentException iae1) { + logger.log(Level.WARNING, String.format("Duplicate field: %s", fieldName), iae1); + return null; + } + } + } + + /* parse posts. */ + SimpleXML postsXml = soneXml.getNode("posts"); + Set posts = new HashSet(); + if (postsXml == null) { + /* TODO - mark Sone as bad. */ + logger.log(Level.WARNING, String.format("Downloaded Sone %s has no posts!", sone)); + } else { + for (SimpleXML postXml : postsXml.getNodes("post")) { + String postId = postXml.getValue("id", null); + String postRecipientId = postXml.getValue("recipient", null); + String postTime = postXml.getValue("time", null); + String postText = postXml.getValue("text", null); + if ((postId == null) || (postTime == null) || (postText == null)) { + /* TODO - mark Sone as bad. */ + logger.log(Level.WARNING, String.format("Downloaded post for Sone %s with missing data! ID: %s, Time: %s, Text: %s", sone, postId, postTime, postText)); + return null; + } + try { + PostBuilder postBuilder = core.postBuilder(); + /* TODO - parse time correctly. */ + postBuilder.withId(postId).from(sone.getId()).withTime(Long.parseLong(postTime)).withText(postText); + if ((postRecipientId != null) && (postRecipientId.length() == 43)) { + postBuilder.to(postRecipientId); + } + posts.add(postBuilder.build()); + } catch (NumberFormatException nfe1) { + /* TODO - mark Sone as bad. */ + logger.log(Level.WARNING, String.format("Downloaded post for Sone %s with invalid time: %s", sone, postTime)); + return null; + } + } + } + + /* parse replies. */ + SimpleXML repliesXml = soneXml.getNode("replies"); + Set replies = new HashSet(); + if (repliesXml == null) { + /* TODO - mark Sone as bad. */ + logger.log(Level.WARNING, String.format("Downloaded Sone %s has no replies!", sone)); + } else { + for (SimpleXML replyXml : repliesXml.getNodes("reply")) { + String replyId = replyXml.getValue("id", null); + String replyPostId = replyXml.getValue("post-id", null); + String replyTime = replyXml.getValue("time", null); + String replyText = replyXml.getValue("text", null); + if ((replyId == null) || (replyPostId == null) || (replyTime == null) || (replyText == null)) { + /* TODO - mark Sone as bad. */ + 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)); + return null; + } + try { + PostReplyBuilder postReplyBuilder = core.postReplyBuilder(); + /* TODO - parse time correctly. */ + postReplyBuilder.withId(replyId).from(sone.getId()).to(replyPostId).withTime(Long.parseLong(replyTime)).withText(replyText); + replies.add(postReplyBuilder.build()); + } catch (NumberFormatException nfe1) { + /* TODO - mark Sone as bad. */ + logger.log(Level.WARNING, String.format("Downloaded reply for Sone %s with invalid time: %s", sone, replyTime)); + return null; + } + } + } + + /* parse liked post IDs. */ + SimpleXML likePostIdsXml = soneXml.getNode("post-likes"); + Set likedPostIds = new HashSet(); + if (likePostIdsXml == null) { + /* TODO - mark Sone as bad. */ + logger.log(Level.WARNING, String.format("Downloaded Sone %s has no post likes!", sone)); + } else { + for (SimpleXML likedPostIdXml : likePostIdsXml.getNodes("post-like")) { + String postId = likedPostIdXml.getValue(); + likedPostIds.add(postId); + } + } + + /* parse liked reply IDs. */ + SimpleXML likeReplyIdsXml = soneXml.getNode("reply-likes"); + Set likedReplyIds = new HashSet(); + if (likeReplyIdsXml == null) { + /* TODO - mark Sone as bad. */ + logger.log(Level.WARNING, String.format("Downloaded Sone %s has no reply likes!", sone)); + } else { + for (SimpleXML likedReplyIdXml : likeReplyIdsXml.getNodes("reply-like")) { + String replyId = likedReplyIdXml.getValue(); + likedReplyIds.add(replyId); + } + } + + /* parse albums. */ + SimpleXML albumsXml = soneXml.getNode("albums"); + Map allImages = new HashMap(); + List topLevelAlbums = new ArrayList(); + if (albumsXml != null) { + for (SimpleXML albumXml : albumsXml.getNodes("album")) { + String id = albumXml.getValue("id", null); + String parentId = albumXml.getValue("parent", null); + String title = albumXml.getValue("title", null); + String description = albumXml.getValue("description", ""); + String albumImageId = albumXml.getValue("album-image", null); + if ((id == null) || (title == null)) { + logger.log(Level.WARNING, String.format("Downloaded Sone %s contains invalid album!", sone)); + return null; + } + Album parent = null; + if (parentId != null) { + parent = core.getAlbum(parentId); + if (parent == null) { + logger.log(Level.WARNING, String.format("Downloaded Sone %s has album with invalid parent!", sone)); + return null; + } + } + Album album = core.albumBuilder() + .withId(id) + .by(sone) + .build() + .modify() + .setTitle(title) + .setDescription(description) + .update(); + if (parent != null) { + parent.addAlbum(album); + } else { + topLevelAlbums.add(album); + } + SimpleXML imagesXml = albumXml.getNode("images"); + if (imagesXml != null) { + for (SimpleXML imageXml : imagesXml.getNodes("image")) { + String imageId = imageXml.getValue("id", null); + String imageCreationTimeString = imageXml.getValue("creation-time", null); + String imageKey = imageXml.getValue("key", null); + String imageTitle = imageXml.getValue("title", null); + String imageDescription = imageXml.getValue("description", ""); + String imageWidthString = imageXml.getValue("width", null); + String imageHeightString = imageXml.getValue("height", null); + if ((imageId == null) || (imageCreationTimeString == null) || (imageKey == null) || (imageTitle == null) || (imageWidthString == null) || (imageHeightString == null)) { + logger.log(Level.WARNING, String.format("Downloaded Sone %s contains invalid images!", sone)); + return null; + } + long creationTime = Numbers.safeParseLong(imageCreationTimeString, 0L); + int imageWidth = Numbers.safeParseInteger(imageWidthString, 0); + int imageHeight = Numbers.safeParseInteger(imageHeightString, 0); + if ((imageWidth < 1) || (imageHeight < 1)) { + logger.log(Level.WARNING, String.format("Downloaded Sone %s contains image %s with invalid dimensions (%s, %s)!", sone, imageId, imageWidthString, imageHeightString)); + return null; + } + Image image = core.imageBuilder().withId(imageId).build().modify().setSone(sone).setKey(imageKey).setCreationTime(creationTime).update(); + image = image.modify().setTitle(imageTitle).setDescription(imageDescription).update(); + image = image.modify().setWidth(imageWidth).setHeight(imageHeight).update(); + album.addImage(image); + allImages.put(imageId, image); + } + } + album.modify().setAlbumImage(albumImageId).update(); + } + } + + /* process avatar. */ + if (avatarId != null) { + profile.setAvatar(allImages.get(avatarId)); + } + + /* okay, apparently everything was parsed correctly. Now import. */ + /* atomic setter operation on the Sone. */ + synchronized (sone) { + sone.setProfile(profile); + sone.setPosts(posts); + sone.setReplies(replies); + sone.setLikePostIds(likedPostIds); + sone.setLikeReplyIds(likedReplyIds); + for (Album album : topLevelAlbums) { + sone.getRootAlbum().addAlbum(album); + } + } + + return sone; + + } + +} diff --git a/src/test/java/net/pterodactylus/sone/core/SoneDownloaderTest.java b/src/test/java/net/pterodactylus/sone/core/SoneDownloaderTest.java index 127837a..33c8ead 100644 --- a/src/test/java/net/pterodactylus/sone/core/SoneDownloaderTest.java +++ b/src/test/java/net/pterodactylus/sone/core/SoneDownloaderTest.java @@ -1,25 +1,16 @@ package net.pterodactylus.sone.core; -import static com.google.common.base.Optional.of; import static freenet.keys.InsertableClientSSK.createRandom; import static java.lang.System.currentTimeMillis; -import static java.util.UUID.randomUUID; import static java.util.concurrent.TimeUnit.DAYS; import static net.pterodactylus.sone.data.Sone.SoneStatus.downloading; import static net.pterodactylus.sone.data.Sone.SoneStatus.idle; import static net.pterodactylus.sone.data.Sone.SoneStatus.unknown; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.not; -import static org.hamcrest.Matchers.notNullValue; -import static org.hamcrest.Matchers.nullValue; import static org.mockito.ArgumentCaptor.forClass; import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyLong; -import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; @@ -28,45 +19,20 @@ import static org.mockito.Mockito.when; import java.io.IOException; import java.io.InputStream; -import java.net.MalformedURLException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; import net.pterodactylus.sone.core.FreenetInterface.Fetched; -import net.pterodactylus.sone.data.Album; -import net.pterodactylus.sone.data.Album.Modifier; -import net.pterodactylus.sone.data.Client; -import net.pterodactylus.sone.data.Image; -import net.pterodactylus.sone.data.Post; -import net.pterodactylus.sone.data.PostReply; -import net.pterodactylus.sone.data.Profile; import net.pterodactylus.sone.data.Sone; import net.pterodactylus.sone.data.Sone.SoneStatus; -import net.pterodactylus.sone.database.AlbumBuilder; -import net.pterodactylus.sone.database.ImageBuilder; -import net.pterodactylus.sone.database.PostBuilder; -import net.pterodactylus.sone.database.PostReplyBuilder; -import net.pterodactylus.sone.database.SoneBuilder; -import net.pterodactylus.sone.database.memory.MemorySoneBuilder; import net.pterodactylus.sone.freenet.wot.Identity; import freenet.client.ClientMetadata; import freenet.client.FetchResult; import freenet.client.async.USKCallback; import freenet.crypt.DummyRandomSource; -import freenet.keys.ClientSSK; import freenet.keys.FreenetURI; import freenet.keys.InsertableClientSSK; import freenet.support.api.Bucket; -import com.google.common.base.Optional; -import com.google.common.collect.ArrayListMultimap; -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.ListMultimap; import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentCaptor; @@ -82,23 +48,10 @@ public class SoneDownloaderTest { private final Core core = mock(Core.class); private final FreenetInterface freenetInterface = mock(FreenetInterface.class); - private final SoneDownloaderImpl soneDownloader = new SoneDownloaderImpl(core, freenetInterface); + private final SoneParser soneParser = mock(SoneParser.class); + private final SoneDownloaderImpl soneDownloader = new SoneDownloaderImpl(core, freenetInterface, soneParser); private FreenetURI requestUri = mock(FreenetURI.class); private Sone sone = mock(Sone.class); - private final PostBuilder postBuilder = mock(PostBuilder.class); - private final List createdPosts = new ArrayList(); - private Post post = mock(Post.class); - private final PostReplyBuilder postReplyBuilder = mock(PostReplyBuilder.class); - private final Set createdPostReplies = new HashSet(); - private PostReply postReply = mock(PostReply.class); - private final AlbumBuilder albumBuilder = mock(AlbumBuilder.class); - private final ListMultimap nestedAlbums = ArrayListMultimap.create(); - private final ListMultimap albumImages = ArrayListMultimap.create(); - private Album album = mock(Album.class); - private final Map albums = new HashMap(); - private final ImageBuilder imageBuilder = mock(ImageBuilder.class); - private Image image = mock(Image.class); - private final Map images = new HashMap(); @Before public void setupSone() { @@ -124,361 +77,6 @@ public class SoneDownloaderTest { when(sone.getTime()).thenReturn(0L); } - @Before - public void setupSoneBuilder() { - when(core.soneBuilder()).thenAnswer(new Answer() { - @Override - public SoneBuilder answer(InvocationOnMock invocation) { - return new MemorySoneBuilder(); - } - }); - } - - @Before - public void setupPost() { - when(post.getRecipientId()).thenReturn(Optional.absent()); - } - - @Before - public void setupPostBuilder() { - when(postBuilder.withId(anyString())).thenAnswer(new Answer() { - @Override - public PostBuilder answer(InvocationOnMock invocation) throws Throwable { - when(post.getId()).thenReturn((String) invocation.getArguments()[0]); - return postBuilder; - } - }); - when(postBuilder.from(anyString())).thenAnswer(new Answer() { - @Override - public PostBuilder answer(InvocationOnMock invocation) throws Throwable { - final Sone sone = mock(Sone.class); - when(sone.getId()).thenReturn((String) invocation.getArguments()[0]); - when(post.getSone()).thenReturn(sone); - return postBuilder; - } - }); - when(postBuilder.withTime(anyLong())).thenAnswer(new Answer() { - @Override - public PostBuilder answer(InvocationOnMock invocation) throws Throwable { - when(post.getTime()).thenReturn((Long) invocation.getArguments()[0]); - return postBuilder; - } - }); - when(postBuilder.withText(anyString())).thenAnswer(new Answer() { - @Override - public PostBuilder answer(InvocationOnMock invocation) throws Throwable { - when(post.getText()).thenReturn((String) invocation.getArguments()[0]); - return postBuilder; - } - }); - when(postBuilder.to(anyString())).thenAnswer(new Answer() { - @Override - public PostBuilder answer(InvocationOnMock invocation) throws Throwable { - when(post.getRecipientId()).thenReturn(of((String) invocation.getArguments()[0])); - return postBuilder; - } - }); - when(postBuilder.build()).thenAnswer(new Answer() { - @Override - public Post answer(InvocationOnMock invocation) throws Throwable { - Post post = SoneDownloaderTest.this.post; - SoneDownloaderTest.this.post = mock(Post.class); - setupPost(); - createdPosts.add(post); - return post; - } - }); - when(core.postBuilder()).thenReturn(postBuilder); - } - - @Before - public void setupPostReplyBuilder() { - when(postReplyBuilder.withId(anyString())).thenAnswer(new Answer() { - @Override - public PostReplyBuilder answer(InvocationOnMock invocation) throws Throwable { - when(postReply.getId()).thenReturn((String) invocation.getArguments()[0]); - return postReplyBuilder; - } - }); - when(postReplyBuilder.from(anyString())).thenAnswer( - new Answer() { - @Override - public PostReplyBuilder answer( - InvocationOnMock invocation) throws Throwable { - Sone sone = when(mock(Sone.class).getId()).thenReturn( - (String) invocation.getArguments()[0]) - .getMock(); - when(postReply.getSone()).thenReturn(sone); - return postReplyBuilder; - } - }); - when(postReplyBuilder.to(anyString())).thenAnswer( - new Answer() { - @Override - public PostReplyBuilder answer( - InvocationOnMock invocation) throws Throwable { - when(postReply.getPostId()).thenReturn( - (String) invocation.getArguments()[0]); - Post post = when(mock(Post.class).getId()).thenReturn( - (String) invocation.getArguments()[0]) - .getMock(); - when(postReply.getPost()).thenReturn(of(post)); - return postReplyBuilder; - } - }); - when(postReplyBuilder.withTime(anyLong())).thenAnswer( - new Answer() { - @Override - public PostReplyBuilder answer( - InvocationOnMock invocation) throws Throwable { - when(postReply.getTime()).thenReturn( - (Long) invocation.getArguments()[0]); - return postReplyBuilder; - } - }); - when(postReplyBuilder.withText(anyString())).thenAnswer(new Answer() { - @Override - public PostReplyBuilder answer(InvocationOnMock invocation) throws Throwable { - when(postReply.getText()).thenReturn((String) invocation.getArguments()[0]); - return postReplyBuilder; - } - }); - when(postReplyBuilder.build()).thenAnswer(new Answer() { - @Override - public PostReply answer(InvocationOnMock invocation) throws Throwable { - PostReply postReply = SoneDownloaderTest.this.postReply; - createdPostReplies.add(postReply); - SoneDownloaderTest.this.postReply = mock(PostReply.class); - return postReply; - } - }); - when(core.postReplyBuilder()).thenReturn(postReplyBuilder); - } - - @Before - public void setupAlbum() { - final Album album = SoneDownloaderTest.this.album; - when(album.getAlbumImage()).thenReturn(mock(Image.class)); - doAnswer(new Answer() { - @Override - public Void answer(InvocationOnMock invocation) { - nestedAlbums.put(album, (Album) invocation.getArguments()[0]); - return null; - } - }).when(album).addAlbum(any(Album.class)); - doAnswer(new Answer() { - @Override - public Void answer(InvocationOnMock invocation) { - albumImages.put(album, (Image) invocation.getArguments()[0]); - return null; - } - }).when(album).addImage(any(Image.class)); - when(album.getAlbums()).thenAnswer(new Answer>() { - @Override - public List answer(InvocationOnMock invocation) { - return nestedAlbums.get(album); - } - }); - when(album.getImages()).thenAnswer(new Answer>() { - @Override - public List answer(InvocationOnMock invocation) { - return albumImages.get(album); - } - }); - final Modifier albumModifier = new Modifier() { - private String title = album.getTitle(); - private String description = album.getDescription(); - private String imageId = album.getAlbumImage().getId(); - - @Override - public Modifier setTitle(String title) { - this.title = title; - return this; - } - - @Override - public Modifier setDescription(String description) { - this.description = description; - return this; - } - - @Override - public Modifier setAlbumImage(String imageId) { - this.imageId = imageId; - return this; - } - - @Override - public Album update() throws IllegalStateException { - when(album.getTitle()).thenReturn(title); - when(album.getDescription()).thenReturn(description); - Image image = mock(Image.class); - when(image.getId()).thenReturn(imageId); - when(album.getAlbumImage()).thenReturn(image); - return album; - } - }; - when(album.modify()).thenReturn(albumModifier); - } - - @Before - public void setupAlbumBuilder() { - when(albumBuilder.withId(anyString())).thenAnswer(new Answer() { - @Override - public AlbumBuilder answer(InvocationOnMock invocation) { - when(album.getId()).thenReturn((String) invocation.getArguments()[0]); - return albumBuilder; - } - }); - when(albumBuilder.randomId()).thenAnswer(new Answer() { - @Override - public AlbumBuilder answer(InvocationOnMock invocation) { - when(album.getId()).thenReturn(randomUUID().toString()); - return albumBuilder; - } - }); - when(albumBuilder.by(any(Sone.class))).thenAnswer(new Answer() { - @Override - public AlbumBuilder answer(InvocationOnMock invocation) { - when(album.getSone()).thenReturn((Sone) invocation.getArguments()[0]); - return albumBuilder; - } - }); - when(albumBuilder.build()).thenAnswer(new Answer() { - @Override - public Album answer(InvocationOnMock invocation) { - Album album = SoneDownloaderTest.this.album; - albums.put(album.getId(), album); - SoneDownloaderTest.this.album = mock(Album.class); - setupAlbum(); - return album; - } - }); - when(core.albumBuilder()).thenReturn(albumBuilder); - } - - @Before - public void setupAlbums() { - when(core.getAlbum(anyString())).thenAnswer(new Answer() { - @Override - public Album answer(InvocationOnMock invocation) - throws Throwable { - return albums.get(invocation.getArguments()[0]); - } - }); - } - - @Before - public void setupImage() { - final Image image = SoneDownloaderTest.this.image; - Image.Modifier modifier = new Image.Modifier() { - private Sone sone = image.getSone(); - private long creationTime = image.getCreationTime(); - private String key = image.getKey(); - private String title = image.getTitle(); - private String description = image.getDescription(); - private int width = image.getWidth(); - private int height = image.getHeight(); - - @Override - public Image.Modifier setSone(Sone sone) { - this.sone = sone; - return this; - } - - @Override - public Image.Modifier setCreationTime(long creationTime) { - this.creationTime = creationTime; - return this; - } - - @Override - public Image.Modifier setKey(String key) { - this.key = key; - return this; - } - - @Override - public Image.Modifier setTitle(String title) { - this.title = title; - return this; - } - - @Override - public Image.Modifier setDescription(String description) { - this.description = description; - return this; - } - - @Override - public Image.Modifier setWidth(int width) { - this.width = width; - return this; - } - - @Override - public Image.Modifier setHeight(int height) { - this.height = height; - return this; - } - - @Override - public Image update() throws IllegalStateException { - when(image.getSone()).thenReturn(sone); - when(image.getCreationTime()).thenReturn(creationTime); - when(image.getKey()).thenReturn(key); - when(image.getTitle()).thenReturn(title); - when(image.getDescription()).thenReturn(description); - when(image.getWidth()).thenReturn(width); - when(image.getHeight()).thenReturn(height); - return image; - } - }; - when(image.getSone()).thenReturn(sone); - when(image.modify()).thenReturn(modifier); - } - - @Before - public void setupImageBuilder() { - when(imageBuilder.randomId()).thenAnswer(new Answer() { - @Override - public ImageBuilder answer(InvocationOnMock invocation) { - when(image.getId()).thenReturn(randomUUID().toString()); - return imageBuilder; - } - }); - when(imageBuilder.withId(anyString())).thenAnswer(new Answer() { - @Override - public ImageBuilder answer(InvocationOnMock invocation) { - when(image.getId()).thenReturn( - (String) invocation.getArguments()[0]); - return imageBuilder; - } - }); - when(imageBuilder.build()).thenAnswer(new Answer() { - @Override - public Image answer(InvocationOnMock invocation) { - Image image = SoneDownloaderTest.this.image; - images.put(image.getId(), image); - SoneDownloaderTest.this.image = mock(Image.class); - setupImage(); - return image; - } - }); - when(core.imageBuilder()).thenReturn(imageBuilder); - } - - @Before - public void setupImages() { - when(core.getImage(anyString())).thenAnswer(new Answer() { - @Override - public Image answer(InvocationOnMock invocation) - throws Throwable { - return images.get(invocation.getArguments()[0]); - } - }); - } - @Test public void addingASoneWillRegisterItsKey() { soneDownloader.addSone(sone); @@ -496,358 +94,6 @@ public class SoneDownloaderTest { verify(freenetInterface).unregisterUsk(sone); } - @Test - public void parsingASoneFailsWhenDocumentIsNotXml() throws SoneException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-not-xml.xml"); - assertThat(soneDownloader.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneFailsWhenDocumentHasNegativeProtocolVersion() throws SoneException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-negative-protocol-version.xml"); - assertThat(soneDownloader.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneFailsWhenProtocolVersionIsTooLarge() throws SoneException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-too-large-protocol-version.xml"); - assertThat(soneDownloader.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneFailsWhenThereIsNoTime() throws SoneException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-no-time.xml"); - assertThat(soneDownloader.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneFailsWhenTimeIsNotNumeric() throws SoneException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-time-not-numeric.xml"); - assertThat(soneDownloader.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneFailsWhenProfileIsMissing() throws SoneException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-no-profile.xml"); - assertThat(soneDownloader.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneFailsWhenProfileFieldIsMissingAFieldName() throws SoneException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-profile-missing-field-name.xml"); - assertThat(soneDownloader.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneFailsWhenProfileFieldNameIsEmpty() throws SoneException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-profile-empty-field-name.xml"); - assertThat(soneDownloader.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneFailsWhenProfileFieldNameIsNotUnique() throws SoneException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-profile-duplicate-field-name.xml"); - assertThat(soneDownloader.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneSucceedsWithoutPayload() throws SoneException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-no-payload.xml"); - assertThat(soneDownloader.parseSone(sone, inputStream).getTime(), is( - 1407197508000L)); - } - - @Test - public void parsingASoneSucceedsWithoutProtocolVersion() throws SoneException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-missing-protocol-version.xml"); - assertThat(soneDownloader.parseSone(sone, inputStream), not( - nullValue())); - } - - @Test - public void parsingASoneFailsWithMissingClientName() throws SoneException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-missing-client-name.xml"); - assertThat(soneDownloader.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneFailsWithMissingClientVersion() throws SoneException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-missing-client-version.xml"); - assertThat(soneDownloader.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneSucceedsWithClientInfo() throws SoneException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-client-info.xml"); - assertThat(soneDownloader.parseSone(sone, inputStream).getClient(), is(new Client("some-client", "some-version"))); - } - - @Test - public void parsingASoneSucceedsWithProfile() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-profile.xml"); - final Profile profile = soneDownloader.parseSone(sone, inputStream).getProfile(); - assertThat(profile.getFirstName(), is("first")); - assertThat(profile.getMiddleName(), is("middle")); - assertThat(profile.getLastName(), is("last")); - assertThat(profile.getBirthDay(), is(18)); - assertThat(profile.getBirthMonth(), is(12)); - assertThat(profile.getBirthYear(), is(1976)); - } - - @Test - public void parsingASoneSucceedsWithoutProfileFields() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-fields.xml"); - assertThat(soneDownloader.parseSone(sone, inputStream), notNullValue()); - } - - @Test - public void parsingASoneFailsWithoutPostId() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-post-id.xml"); - assertThat(soneDownloader.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneFailsWithoutPostTime() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-post-time.xml"); - assertThat(soneDownloader.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneFailsWithoutPostText() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-post-text.xml"); - assertThat(soneDownloader.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneFailsWithInvalidPostTime() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-invalid-post-time.xml"); - assertThat(soneDownloader.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneSucceedsWithValidPostTime() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-valid-post-time.xml"); - final List posts = soneDownloader.parseSone(sone, inputStream).getPosts(); - assertThat(posts, is(createdPosts)); - assertThat(posts.get(0).getSone().getId(), is(sone.getId())); - assertThat(posts.get(0).getId(), is("post-id")); - assertThat(posts.get(0).getTime(), is(1407197508000L)); - assertThat(posts.get(0).getRecipientId(), is(Optional.absent())); - assertThat(posts.get(0).getText(), is("text")); - } - - @Test - public void parsingASoneSucceedsWithRecipient() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-recipient.xml"); - final List posts = soneDownloader.parseSone(sone, inputStream).getPosts(); - assertThat(posts, is(createdPosts)); - assertThat(posts.get(0).getSone().getId(), is(sone.getId())); - assertThat(posts.get(0).getId(), is("post-id")); - assertThat(posts.get(0).getTime(), is(1407197508000L)); - assertThat(posts.get(0).getRecipientId(), is(of( - "1234567890123456789012345678901234567890123"))); - assertThat(posts.get(0).getText(), is("text")); - } - - @Test - public void parsingASoneSucceedsWithInvalidRecipient() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-invalid-recipient.xml"); - final List posts = soneDownloader.parseSone(sone, inputStream).getPosts(); - assertThat(posts, is(createdPosts)); - assertThat(posts.get(0).getSone().getId(), is(sone.getId())); - assertThat(posts.get(0).getId(), is("post-id")); - assertThat(posts.get(0).getTime(), is(1407197508000L)); - assertThat(posts.get(0).getRecipientId(), is(Optional.absent())); - assertThat(posts.get(0).getText(), is("text")); - } - - @Test - public void parsingASoneFailsWithoutPostReplyId() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-post-reply-id.xml"); - assertThat(soneDownloader.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneFailsWithoutPostReplyPostId() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-post-reply-post-id.xml"); - assertThat(soneDownloader.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneFailsWithoutPostReplyTime() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-post-reply-time.xml"); - assertThat(soneDownloader.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneFailsWithoutPostReplyText() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-post-reply-text.xml"); - assertThat(soneDownloader.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneFailsWithInvalidPostReplyTime() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-invalid-post-reply-time.xml"); - assertThat(soneDownloader.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneSucceedsWithValidPostReplyTime() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-valid-post-reply-time.xml"); - final Set postReplies = soneDownloader.parseSone(sone, inputStream).getReplies(); - assertThat(postReplies, is(createdPostReplies)); - PostReply postReply = createdPostReplies.iterator().next(); - assertThat(postReply.getId(), is("reply-id")); - assertThat(postReply.getPostId(), is("post-id")); - assertThat(postReply.getPost().get().getId(), is("post-id")); - assertThat(postReply.getSone().getId(), is("identity")); - assertThat(postReply.getTime(), is(1407197508000L)); - assertThat(postReply.getText(), is("reply-text")); - } - - @Test - public void parsingASoneSucceedsWithoutLikedPostIds() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-liked-post-ids.xml"); - assertThat(soneDownloader.parseSone(sone, inputStream), not( - nullValue())); - } - - @Test - public void parsingASoneSucceedsWithLikedPostIds() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-liked-post-ids.xml"); - assertThat(soneDownloader.parseSone(sone, inputStream).getLikedPostIds(), is( - (Set) ImmutableSet.of("liked-post-id"))); - } - - @Test - public void parsingASoneSucceedsWithoutLikedPostReplyIds() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-liked-post-reply-ids.xml"); - assertThat(soneDownloader.parseSone(sone, inputStream), not( - nullValue())); - } - - @Test - public void parsingASoneSucceedsWithLikedPostReplyIds() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-liked-post-reply-ids.xml"); - assertThat(soneDownloader.parseSone(sone, inputStream).getLikedReplyIds(), is( - (Set) ImmutableSet.of("liked-post-reply-id"))); - } - - @Test - public void parsingASoneSucceedsWithoutAlbums() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-albums.xml"); - assertThat(soneDownloader.parseSone(sone, inputStream), not( - nullValue())); - } - - @Test - public void parsingASoneFailsWithoutAlbumId() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-album-id.xml"); - assertThat(soneDownloader.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneFailsWithoutAlbumTitle() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-album-title.xml"); - assertThat(soneDownloader.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneSucceedsWithNestedAlbums() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-multiple-albums.xml"); - final Sone parsedSone = soneDownloader.parseSone(sone, inputStream); - assertThat(parsedSone, not(nullValue())); - assertThat(parsedSone.getRootAlbum().getAlbums(), hasSize(1)); - Album album = parsedSone.getRootAlbum().getAlbums().get(0); - assertThat(album.getId(), is("album-id-1")); - assertThat(album.getTitle(), is("album-title")); - assertThat(album.getDescription(), is("album-description")); - assertThat(album.getAlbums(), hasSize(1)); - Album nestedAlbum = album.getAlbums().get(0); - assertThat(nestedAlbum.getId(), is("album-id-2")); - assertThat(nestedAlbum.getTitle(), is("album-title-2")); - assertThat(nestedAlbum.getDescription(), is("album-description-2")); - assertThat(nestedAlbum.getAlbums(), hasSize(0)); - } - - @Test - public void parsingASoneFailsWithInvalidParentAlbumId() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-invalid-parent-album-id.xml"); - assertThat(soneDownloader.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneSucceedsWithoutImages() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-images.xml"); - assertThat(soneDownloader.parseSone(sone, inputStream), not( - nullValue())); - } - - @Test - public void parsingASoneFailsWithoutImageId() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-image-id.xml"); - assertThat(soneDownloader.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneFailsWithoutImageTime() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-image-time.xml"); - assertThat(soneDownloader.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneFailsWithoutImageKey() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-image-key.xml"); - assertThat(soneDownloader.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneFailsWithoutImageTitle() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-image-title.xml"); - assertThat(soneDownloader.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneFailsWithoutImageWidth() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-image-width.xml"); - assertThat(soneDownloader.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneFailsWithoutImageHeight() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-image-height.xml"); - assertThat(soneDownloader.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneFailsWithInvalidImageWidth() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-invalid-image-width.xml"); - assertThat(soneDownloader.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneFailsWithInvalidImageHeight() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-invalid-image-height.xml"); - assertThat(soneDownloader.parseSone(sone, inputStream), nullValue()); - } - - @Test - public void parsingASoneSucceedsWithImage() throws SoneException, MalformedURLException { - InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-image.xml"); - final Sone sone = soneDownloader.parseSone(this.sone, inputStream); - assertThat(sone, not(nullValue())); - assertThat(sone.getRootAlbum().getAlbums(), hasSize(1)); - assertThat(sone.getRootAlbum().getAlbums().get(0).getImages(), hasSize(1)); - Image image = sone.getRootAlbum().getAlbums().get(0).getImages().get(0); - assertThat(image.getId(), is("image-id")); - assertThat(image.getCreationTime(), is(1407197508000L)); - assertThat(image.getKey(), is("KSK@GPLv3.txt")); - assertThat(image.getTitle(), is("image-title")); - assertThat(image.getDescription(), is("image-description")); - assertThat(image.getWidth(), is(1920)); - assertThat(image.getHeight(), is(1080)); - assertThat(sone.getProfile().getAvatar(), is("image-id")); - } @Test public void stoppingTheSoneDownloaderUnregistersTheSone() { @@ -914,23 +160,6 @@ public class SoneDownloaderTest { } @Test - public void successfulFetchingOfSoneWithUskRequestUriUpdatesTheCoreWithASone() throws IOException { - FreenetURI finalRequestUri = requestUri.sskForUSK() - .setMetaString(new String[] { "sone.xml" }); - final Fetched fetchResult = createFetchResult(finalRequestUri, - getClass().getResourceAsStream("sone-parser-no-payload.xml")); - when(freenetInterface.fetchUri(finalRequestUri)).thenReturn(fetchResult); - soneDownloader.fetchSoneAction(sone).run(); - verifyThatParsedSoneHasTheSameIdAsTheOriginalSone(); - } - - private void verifyThatParsedSoneHasTheSameIdAsTheOriginalSone() { - ArgumentCaptor soneCaptor = forClass(Sone.class); - verify(core).updateSone(soneCaptor.capture()); - assertThat(soneCaptor.getValue().getId(), is(sone.getId())); - } - - @Test public void fetchingSoneWithInvalidXmlWillNotUpdateTheCore() throws IOException { final Fetched fetchResult = createFetchResult(requestUri, getClass().getResourceAsStream("sone-parser-not-xml.xml")); when(freenetInterface.fetchUri(requestUri)).thenReturn(fetchResult); diff --git a/src/test/java/net/pterodactylus/sone/core/SoneParserTest.java b/src/test/java/net/pterodactylus/sone/core/SoneParserTest.java new file mode 100644 index 0000000..b6560f4 --- /dev/null +++ b/src/test/java/net/pterodactylus/sone/core/SoneParserTest.java @@ -0,0 +1,832 @@ +package net.pterodactylus.sone.core; + +import static com.google.common.base.Optional.of; +import static freenet.keys.InsertableClientSSK.createRandom; +import static java.lang.System.currentTimeMillis; +import static java.util.UUID.randomUUID; +import static java.util.concurrent.TimeUnit.DAYS; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyLong; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.InputStream; +import java.net.MalformedURLException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import net.pterodactylus.sone.data.Album; +import net.pterodactylus.sone.data.Album.Modifier; +import net.pterodactylus.sone.data.Client; +import net.pterodactylus.sone.data.Image; +import net.pterodactylus.sone.data.Post; +import net.pterodactylus.sone.data.PostReply; +import net.pterodactylus.sone.data.Profile; +import net.pterodactylus.sone.data.Sone; +import net.pterodactylus.sone.database.AlbumBuilder; +import net.pterodactylus.sone.database.ImageBuilder; +import net.pterodactylus.sone.database.PostBuilder; +import net.pterodactylus.sone.database.PostReplyBuilder; +import net.pterodactylus.sone.database.SoneBuilder; +import net.pterodactylus.sone.database.memory.MemorySoneBuilder; +import net.pterodactylus.sone.freenet.wot.Identity; +import net.pterodactylus.sone.freenet.wot.OwnIdentity; + +import freenet.crypt.DummyRandomSource; +import freenet.keys.FreenetURI; +import freenet.keys.InsertableClientSSK; + +import com.google.common.base.Optional; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ListMultimap; +import org.junit.Before; +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +/** + * Unit test for {@link SoneParser}. + * + * @author David ‘Bombe’ Roden + */ +public class SoneParserTest { + + private final Core core = mock(Core.class); + private final SoneParser soneParser = new SoneParser(core); + private final Sone sone = mock(Sone.class); + private FreenetURI requestUri = mock(FreenetURI.class); + private final PostBuilder postBuilder = mock(PostBuilder.class); + private final List createdPosts = new ArrayList(); + private Post post = mock(Post.class); + private final PostReplyBuilder postReplyBuilder = mock(PostReplyBuilder.class); + private final Set createdPostReplies = new HashSet(); + private PostReply postReply = mock(PostReply.class); + private final AlbumBuilder albumBuilder = mock(AlbumBuilder.class); + private final ListMultimap + nestedAlbums = ArrayListMultimap.create(); + private final ListMultimap albumImages = ArrayListMultimap.create(); + private Album album = mock(Album.class); + private final Map albums = new HashMap(); + private final ImageBuilder imageBuilder = mock(ImageBuilder.class); + private Image image = mock(Image.class); + private final Map images = new HashMap(); + + @Before + public void setupSone() { + setupSone(this.sone, Identity.class); + } + + private void setupSone(Sone sone, Class identityClass) { + Identity identity = mock(identityClass); + InsertableClientSSK clientSSK = + createRandom(new DummyRandomSource(), "WoT"); + when(identity.getRequestUri()).thenReturn(clientSSK.getURI().toString()); + when(identity.getId()).thenReturn("identity"); + when(sone.getId()).thenReturn("identity"); + when(sone.getIdentity()).thenReturn(identity); + requestUri = clientSSK.getURI().setKeyType("USK").setDocName("Sone"); + when(sone.getRequestUri()).thenAnswer(new Answer() { + @Override + public FreenetURI answer(InvocationOnMock invocation) + throws Throwable { + return requestUri; + } + }); + when(sone.getTime()) + .thenReturn(currentTimeMillis() - DAYS.toMillis(1)); + } + + @Before + public void setupSoneBuilder() { + when(core.soneBuilder()).thenAnswer(new Answer() { + @Override + public SoneBuilder answer(InvocationOnMock invocation) { + return new MemorySoneBuilder(); + } + }); + } + + @Before + public void setupPost() { + when(post.getRecipientId()).thenReturn(Optional.absent()); + } + + @Before + public void setupPostBuilder() { + when(postBuilder.withId(anyString())).thenAnswer(new Answer() { + @Override + public PostBuilder answer(InvocationOnMock invocation) throws Throwable { + when(post.getId()).thenReturn((String) invocation.getArguments()[0]); + return postBuilder; + } + }); + when(postBuilder.from(anyString())).thenAnswer(new Answer() { + @Override + public PostBuilder answer(InvocationOnMock invocation) throws Throwable { + final Sone sone = mock(Sone.class); + when(sone.getId()).thenReturn((String) invocation.getArguments()[0]); + when(post.getSone()).thenReturn(sone); + return postBuilder; + } + }); + when(postBuilder.withTime(anyLong())).thenAnswer(new Answer() { + @Override + public PostBuilder answer(InvocationOnMock invocation) throws Throwable { + when(post.getTime()).thenReturn((Long) invocation.getArguments()[0]); + return postBuilder; + } + }); + when(postBuilder.withText(anyString())).thenAnswer(new Answer() { + @Override + public PostBuilder answer(InvocationOnMock invocation) throws Throwable { + when(post.getText()).thenReturn((String) invocation.getArguments()[0]); + return postBuilder; + } + }); + when(postBuilder.to(anyString())).thenAnswer(new Answer() { + @Override + public PostBuilder answer(InvocationOnMock invocation) throws Throwable { + when(post.getRecipientId()).thenReturn(of((String) invocation.getArguments()[0])); + return postBuilder; + } + }); + when(postBuilder.build()).thenAnswer(new Answer() { + @Override + public Post answer(InvocationOnMock invocation) throws Throwable { + Post post = SoneParserTest.this.post; + SoneParserTest.this.post = mock(Post.class); + setupPost(); + createdPosts.add(post); + return post; + } + }); + when(core.postBuilder()).thenReturn(postBuilder); + } + + @Before + public void setupPostReplyBuilder() { + when(postReplyBuilder.withId(anyString())).thenAnswer(new Answer() { + @Override + public PostReplyBuilder answer(InvocationOnMock invocation) throws Throwable { + when(postReply.getId()).thenReturn((String) invocation.getArguments()[0]); + return postReplyBuilder; + } + }); + when(postReplyBuilder.from(anyString())).thenAnswer( + new Answer() { + @Override + public PostReplyBuilder answer( + InvocationOnMock invocation) throws Throwable { + Sone sone = when(mock(Sone.class).getId()).thenReturn( + (String) invocation.getArguments()[0]) + .getMock(); + when(postReply.getSone()).thenReturn(sone); + return postReplyBuilder; + } + }); + when(postReplyBuilder.to(anyString())).thenAnswer( + new Answer() { + @Override + public PostReplyBuilder answer( + InvocationOnMock invocation) throws Throwable { + when(postReply.getPostId()).thenReturn( + (String) invocation.getArguments()[0]); + Post post = when(mock(Post.class).getId()).thenReturn( + (String) invocation.getArguments()[0]) + .getMock(); + when(postReply.getPost()).thenReturn(of(post)); + return postReplyBuilder; + } + }); + when(postReplyBuilder.withTime(anyLong())).thenAnswer( + new Answer() { + @Override + public PostReplyBuilder answer( + InvocationOnMock invocation) throws Throwable { + when(postReply.getTime()).thenReturn( + (Long) invocation.getArguments()[0]); + return postReplyBuilder; + } + }); + when(postReplyBuilder.withText(anyString())).thenAnswer(new Answer() { + @Override + public PostReplyBuilder answer(InvocationOnMock invocation) throws Throwable { + when(postReply.getText()).thenReturn((String) invocation.getArguments()[0]); + return postReplyBuilder; + } + }); + when(postReplyBuilder.build()).thenAnswer(new Answer() { + @Override + public PostReply answer(InvocationOnMock invocation) throws Throwable { + PostReply postReply = SoneParserTest.this.postReply; + createdPostReplies.add(postReply); + SoneParserTest.this.postReply = mock(PostReply.class); + return postReply; + } + }); + when(core.postReplyBuilder()).thenReturn(postReplyBuilder); + } + + @Before + public void setupAlbum() { + final Album album = SoneParserTest.this.album; + when(album.getAlbumImage()).thenReturn(mock(Image.class)); + doAnswer(new Answer() { + @Override + public Void answer(InvocationOnMock invocation) { + nestedAlbums.put(album, (Album) invocation.getArguments()[0]); + return null; + } + }).when(album).addAlbum(any(Album.class)); + doAnswer(new Answer() { + @Override + public Void answer(InvocationOnMock invocation) { + albumImages.put(album, (Image) invocation.getArguments()[0]); + return null; + } + }).when(album).addImage(any(Image.class)); + when(album.getAlbums()).thenAnswer(new Answer>() { + @Override + public List answer(InvocationOnMock invocation) { + return nestedAlbums.get(album); + } + }); + when(album.getImages()).thenAnswer(new Answer>() { + @Override + public List answer(InvocationOnMock invocation) { + return albumImages.get(album); + } + }); + final Modifier albumModifier = new Modifier() { + private String title = album.getTitle(); + private String description = album.getDescription(); + private String imageId = album.getAlbumImage().getId(); + + @Override + public Modifier setTitle(String title) { + this.title = title; + return this; + } + + @Override + public Modifier setDescription(String description) { + this.description = description; + return this; + } + + @Override + public Modifier setAlbumImage(String imageId) { + this.imageId = imageId; + return this; + } + + @Override + public Album update() throws IllegalStateException { + when(album.getTitle()).thenReturn(title); + when(album.getDescription()).thenReturn(description); + Image image = mock(Image.class); + when(image.getId()).thenReturn(imageId); + when(album.getAlbumImage()).thenReturn(image); + return album; + } + }; + when(album.modify()).thenReturn(albumModifier); + } + + @Before + public void setupAlbumBuilder() { + when(albumBuilder.withId(anyString())).thenAnswer(new Answer() { + @Override + public AlbumBuilder answer(InvocationOnMock invocation) { + when(album.getId()).thenReturn((String) invocation.getArguments()[0]); + return albumBuilder; + } + }); + when(albumBuilder.randomId()).thenAnswer(new Answer() { + @Override + public AlbumBuilder answer(InvocationOnMock invocation) { + when(album.getId()).thenReturn(randomUUID().toString()); + return albumBuilder; + } + }); + when(albumBuilder.by(any(Sone.class))).thenAnswer(new Answer() { + @Override + public AlbumBuilder answer(InvocationOnMock invocation) { + when(album.getSone()).thenReturn((Sone) invocation.getArguments()[0]); + return albumBuilder; + } + }); + when(albumBuilder.build()).thenAnswer(new Answer() { + @Override + public Album answer(InvocationOnMock invocation) { + Album album = SoneParserTest.this.album; + albums.put(album.getId(), album); + SoneParserTest.this.album = mock(Album.class); + setupAlbum(); + return album; + } + }); + when(core.albumBuilder()).thenReturn(albumBuilder); + } + + @Before + public void setupAlbums() { + when(core.getAlbum(anyString())).thenAnswer(new Answer() { + @Override + public Album answer(InvocationOnMock invocation) + throws Throwable { + return albums.get(invocation.getArguments()[0]); + } + }); + } + + @Before + public void setupImage() { + final Image image = SoneParserTest.this.image; + Image.Modifier modifier = new Image.Modifier() { + private Sone sone = image.getSone(); + private long creationTime = image.getCreationTime(); + private String key = image.getKey(); + private String title = image.getTitle(); + private String description = image.getDescription(); + private int width = image.getWidth(); + private int height = image.getHeight(); + + @Override + public Image.Modifier setSone(Sone sone) { + this.sone = sone; + return this; + } + + @Override + public Image.Modifier setCreationTime(long creationTime) { + this.creationTime = creationTime; + return this; + } + + @Override + public Image.Modifier setKey(String key) { + this.key = key; + return this; + } + + @Override + public Image.Modifier setTitle(String title) { + this.title = title; + return this; + } + + @Override + public Image.Modifier setDescription(String description) { + this.description = description; + return this; + } + + @Override + public Image.Modifier setWidth(int width) { + this.width = width; + return this; + } + + @Override + public Image.Modifier setHeight(int height) { + this.height = height; + return this; + } + + @Override + public Image update() throws IllegalStateException { + when(image.getSone()).thenReturn(sone); + when(image.getCreationTime()).thenReturn(creationTime); + when(image.getKey()).thenReturn(key); + when(image.getTitle()).thenReturn(title); + when(image.getDescription()).thenReturn(description); + when(image.getWidth()).thenReturn(width); + when(image.getHeight()).thenReturn(height); + return image; + } + }; + when(image.getSone()).thenReturn(sone); + when(image.modify()).thenReturn(modifier); + } + + @Before + public void setupImageBuilder() { + when(imageBuilder.randomId()).thenAnswer(new Answer() { + @Override + public ImageBuilder answer(InvocationOnMock invocation) { + when(image.getId()).thenReturn(randomUUID().toString()); + return imageBuilder; + } + }); + when(imageBuilder.withId(anyString())).thenAnswer(new Answer() { + @Override + public ImageBuilder answer(InvocationOnMock invocation) { + when(image.getId()).thenReturn( + (String) invocation.getArguments()[0]); + return imageBuilder; + } + }); + when(imageBuilder.build()).thenAnswer(new Answer() { + @Override + public Image answer(InvocationOnMock invocation) { + Image image = SoneParserTest.this.image; + images.put(image.getId(), image); + SoneParserTest.this.image = mock(Image.class); + setupImage(); + return image; + } + }); + when(core.imageBuilder()).thenReturn(imageBuilder); + } + + @Before + public void setupImages() { + when(core.getImage(anyString())).thenAnswer(new Answer() { + @Override + public Image answer(InvocationOnMock invocation) + throws Throwable { + return images.get(invocation.getArguments()[0]); + } + }); + } + @Test + public void parsingASoneFailsWhenDocumentIsNotXml() throws SoneException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-not-xml.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneFailsWhenDocumentHasNegativeProtocolVersion() throws SoneException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-negative-protocol-version.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneFailsWhenProtocolVersionIsTooLarge() throws SoneException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-too-large-protocol-version.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneFailsWhenThereIsNoTime() throws SoneException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-no-time.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneFailsWhenTimeIsNotNumeric() throws SoneException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-time-not-numeric.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneFailsWhenProfileIsMissing() throws SoneException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-no-profile.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneFailsWhenProfileFieldIsMissingAFieldName() throws SoneException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-profile-missing-field-name.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneFailsWhenProfileFieldNameIsEmpty() throws SoneException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-profile-empty-field-name.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneFailsWhenProfileFieldNameIsNotUnique() throws SoneException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-profile-duplicate-field-name.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneSucceedsWithoutPayload() throws SoneException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-no-payload.xml"); + assertThat(soneParser.parseSone(sone, inputStream).getTime(), is( + 1407197508000L)); + } + + @Test + public void parsingALocalSoneSucceedsWithoutPayload() throws SoneException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-no-payload.xml"); + Sone localSone = mock(Sone.class); + setupSone(localSone, OwnIdentity.class); + when(localSone.isLocal()).thenReturn(true); + Sone parsedSone = soneParser.parseSone(localSone, inputStream); + assertThat(parsedSone.getTime(), is(1407197508000L)); + assertThat(parsedSone.isLocal(), is(true)); + } + + @Test + public void parsingASoneSucceedsWithoutProtocolVersion() throws SoneException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-missing-protocol-version.xml"); + assertThat(soneParser.parseSone(sone, inputStream), not( + nullValue())); + } + + @Test + public void parsingASoneFailsWithMissingClientName() throws SoneException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-missing-client-name.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneFailsWithMissingClientVersion() throws SoneException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-missing-client-version.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneSucceedsWithClientInfo() throws SoneException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-client-info.xml"); + assertThat(soneParser.parseSone(sone, inputStream).getClient(), is(new Client("some-client", "some-version"))); + } + + @Test + public void parsingASoneSucceedsWithProfile() throws SoneException, + MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-profile.xml"); + final Profile profile = soneParser.parseSone(sone, inputStream).getProfile(); + assertThat(profile.getFirstName(), is("first")); + assertThat(profile.getMiddleName(), is("middle")); + assertThat(profile.getLastName(), is("last")); + assertThat(profile.getBirthDay(), is(18)); + assertThat(profile.getBirthMonth(), is(12)); + assertThat(profile.getBirthYear(), is(1976)); + } + + @Test + public void parsingASoneSucceedsWithoutProfileFields() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-fields.xml"); + assertThat(soneParser.parseSone(sone, inputStream), notNullValue()); + } + + @Test + public void parsingASoneFailsWithoutPostId() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-post-id.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneFailsWithoutPostTime() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-post-time.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneFailsWithoutPostText() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-post-text.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneFailsWithInvalidPostTime() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-invalid-post-time.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneSucceedsWithValidPostTime() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-valid-post-time.xml"); + final List posts = soneParser.parseSone(sone, inputStream).getPosts(); + assertThat(posts, is(createdPosts)); + assertThat(posts.get(0).getSone().getId(), is(sone.getId())); + assertThat(posts.get(0).getId(), is("post-id")); + assertThat(posts.get(0).getTime(), is(1407197508000L)); + assertThat(posts.get(0).getRecipientId(), is(Optional.absent())); + assertThat(posts.get(0).getText(), is("text")); + } + + @Test + public void parsingASoneSucceedsWithRecipient() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-recipient.xml"); + final List posts = soneParser.parseSone(sone, inputStream).getPosts(); + assertThat(posts, is(createdPosts)); + assertThat(posts.get(0).getSone().getId(), is(sone.getId())); + assertThat(posts.get(0).getId(), is("post-id")); + assertThat(posts.get(0).getTime(), is(1407197508000L)); + assertThat(posts.get(0).getRecipientId(), is(of( + "1234567890123456789012345678901234567890123"))); + assertThat(posts.get(0).getText(), is("text")); + } + + @Test + public void parsingASoneSucceedsWithInvalidRecipient() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-invalid-recipient.xml"); + final List posts = soneParser.parseSone(sone, inputStream).getPosts(); + assertThat(posts, is(createdPosts)); + assertThat(posts.get(0).getSone().getId(), is(sone.getId())); + assertThat(posts.get(0).getId(), is("post-id")); + assertThat(posts.get(0).getTime(), is(1407197508000L)); + assertThat(posts.get(0).getRecipientId(), is(Optional.absent())); + assertThat(posts.get(0).getText(), is("text")); + } + + @Test + public void parsingASoneFailsWithoutPostReplyId() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-post-reply-id.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneFailsWithoutPostReplyPostId() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-post-reply-post-id.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneFailsWithoutPostReplyTime() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-post-reply-time.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneFailsWithoutPostReplyText() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-post-reply-text.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneFailsWithInvalidPostReplyTime() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-invalid-post-reply-time.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneSucceedsWithValidPostReplyTime() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-valid-post-reply-time.xml"); + final Set postReplies = soneParser.parseSone(sone, inputStream).getReplies(); + assertThat(postReplies, is(createdPostReplies)); + PostReply postReply = createdPostReplies.iterator().next(); + assertThat(postReply.getId(), is("reply-id")); + assertThat(postReply.getPostId(), is("post-id")); + assertThat(postReply.getPost().get().getId(), is("post-id")); + assertThat(postReply.getSone().getId(), is("identity")); + assertThat(postReply.getTime(), is(1407197508000L)); + assertThat(postReply.getText(), is("reply-text")); + } + + @Test + public void parsingASoneSucceedsWithoutLikedPostIds() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-liked-post-ids.xml"); + assertThat(soneParser.parseSone(sone, inputStream), not( + nullValue())); + } + + @Test + public void parsingASoneSucceedsWithLikedPostIds() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-liked-post-ids.xml"); + assertThat(soneParser.parseSone(sone, inputStream).getLikedPostIds(), is( + (Set) ImmutableSet.of("liked-post-id"))); + } + + @Test + public void parsingASoneSucceedsWithoutLikedPostReplyIds() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-liked-post-reply-ids.xml"); + assertThat(soneParser.parseSone(sone, inputStream), not( + nullValue())); + } + + @Test + public void parsingASoneSucceedsWithLikedPostReplyIds() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-liked-post-reply-ids.xml"); + assertThat(soneParser.parseSone(sone, inputStream).getLikedReplyIds(), is( + (Set) ImmutableSet.of("liked-post-reply-id"))); + } + + @Test + public void parsingASoneSucceedsWithoutAlbums() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-albums.xml"); + assertThat(soneParser.parseSone(sone, inputStream), not( + nullValue())); + } + + @Test + public void parsingASoneFailsWithoutAlbumId() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-album-id.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneFailsWithoutAlbumTitle() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-album-title.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneSucceedsWithNestedAlbums() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-multiple-albums.xml"); + final Sone parsedSone = soneParser.parseSone(sone, inputStream); + assertThat(parsedSone, not(nullValue())); + assertThat(parsedSone.getRootAlbum().getAlbums(), hasSize(1)); + Album album = parsedSone.getRootAlbum().getAlbums().get(0); + assertThat(album.getId(), is("album-id-1")); + assertThat(album.getTitle(), is("album-title")); + assertThat(album.getDescription(), is("album-description")); + assertThat(album.getAlbums(), hasSize(1)); + Album nestedAlbum = album.getAlbums().get(0); + assertThat(nestedAlbum.getId(), is("album-id-2")); + assertThat(nestedAlbum.getTitle(), is("album-title-2")); + assertThat(nestedAlbum.getDescription(), is("album-description-2")); + assertThat(nestedAlbum.getAlbums(), hasSize(0)); + } + + @Test + public void parsingASoneFailsWithInvalidParentAlbumId() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-invalid-parent-album-id.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneSucceedsWithoutImages() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-images.xml"); + assertThat(soneParser.parseSone(sone, inputStream), not( + nullValue())); + } + + @Test + public void parsingASoneFailsWithoutImageId() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-image-id.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneFailsWithoutImageTime() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-image-time.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneFailsWithoutImageKey() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-image-key.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneFailsWithoutImageTitle() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-image-title.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneFailsWithoutImageWidth() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-image-width.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneFailsWithoutImageHeight() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-without-image-height.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneFailsWithInvalidImageWidth() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-invalid-image-width.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneFailsWithInvalidImageHeight() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-invalid-image-height.xml"); + assertThat(soneParser.parseSone(sone, inputStream), nullValue()); + } + + @Test + public void parsingASoneSucceedsWithImage() throws SoneException, MalformedURLException { + InputStream inputStream = getClass().getResourceAsStream("sone-parser-with-image.xml"); + final Sone sone = soneParser.parseSone(this.sone, inputStream); + assertThat(sone, not(nullValue())); + assertThat(sone.getRootAlbum().getAlbums(), hasSize(1)); + assertThat(sone.getRootAlbum().getAlbums().get(0).getImages(), hasSize(1)); + Image image = sone.getRootAlbum().getAlbums().get(0).getImages().get(0); + assertThat(image.getId(), is("image-id")); + assertThat(image.getCreationTime(), is(1407197508000L)); + assertThat(image.getKey(), is("KSK@GPLv3.txt")); + assertThat(image.getTitle(), is("image-title")); + assertThat(image.getDescription(), is("image-description")); + assertThat(image.getWidth(), is(1920)); + assertThat(image.getHeight(), is(1080)); + assertThat(sone.getProfile().getAvatar(), is("image-id")); + } + + +} -- 2.7.4