Start moving parsing a Sone from a configuration to a specialized parser.
authorDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Sun, 7 Sep 2014 17:16:27 +0000 (19:16 +0200)
committerDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Sun, 7 Sep 2014 17:16:27 +0000 (19:16 +0200)
src/main/java/net/pterodactylus/sone/core/ConfigurationSoneParser.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/core/Core.java
src/test/java/net/pterodactylus/sone/Matchers.java
src/test/java/net/pterodactylus/sone/core/ConfigurationSoneParserTest.java [new file with mode: 0644]

diff --git a/src/main/java/net/pterodactylus/sone/core/ConfigurationSoneParser.java b/src/main/java/net/pterodactylus/sone/core/ConfigurationSoneParser.java
new file mode 100644 (file)
index 0000000..8f09b3b
--- /dev/null
@@ -0,0 +1,111 @@
+package net.pterodactylus.sone.core;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.sone.data.Profile;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.database.PostBuilder;
+import net.pterodactylus.sone.database.PostBuilderFactory;
+import net.pterodactylus.util.config.Configuration;
+
+/**
+ * Parses a {@link Sone}’s data from a {@link Configuration}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class ConfigurationSoneParser {
+
+       private final Configuration configuration;
+       private final Sone sone;
+       private final String sonePrefix;
+
+       public ConfigurationSoneParser(Configuration configuration, Sone sone) {
+               this.configuration = configuration;
+               this.sone = sone;
+               sonePrefix = "Sone/" + sone.getId();
+       }
+
+       public Profile parseProfile() {
+               Profile profile = new Profile(sone);
+               profile.setFirstName(getString("/Profile/FirstName", null));
+               profile.setMiddleName(getString("/Profile/MiddleName", null));
+               profile.setLastName(getString("/Profile/LastName", null));
+               profile.setBirthDay(getInt("/Profile/BirthDay", null));
+               profile.setBirthMonth(getInt("/Profile/BirthMonth", null));
+               profile.setBirthYear(getInt("/Profile/BirthYear", null));
+
+               /* load profile fields. */
+               int fieldCount = 0;
+               while (true) {
+                       String fieldPrefix = "/Profile/Fields/" + fieldCount++;
+                       String fieldName = getString(fieldPrefix + "/Name", null);
+                       if (fieldName == null) {
+                               break;
+                       }
+                       String fieldValue = getString(fieldPrefix + "/Value", "");
+                       profile.addField(fieldName).setValue(fieldValue);
+               }
+
+               return profile;
+       }
+
+       private String getString(String nodeName, @Nullable String defaultValue) {
+               return configuration.getStringValue(sonePrefix + nodeName)
+                               .getValue(defaultValue);
+       }
+
+       private Integer getInt(String nodeName, @Nullable Integer defaultValue) {
+               return configuration.getIntValue(sonePrefix + nodeName)
+                               .getValue(defaultValue);
+       }
+
+       private Long getLong(String nodeName, @Nullable Long defaultValue) {
+               return configuration.getLongValue(sonePrefix + nodeName)
+                               .getValue(defaultValue);
+       }
+
+       public Collection<Post> parsePosts(PostBuilderFactory postBuilderFactory)
+       throws InvalidPostFound {
+               Set<Post> posts = new HashSet<Post>();
+               while (true) {
+                       String postPrefix = "/Posts/" + posts.size();
+                       String postId = getString(postPrefix + "/ID", null);
+                       if (postId == null) {
+                               break;
+                       }
+                       long postTime = getLong(postPrefix + "/Time", 0L);
+                       String postText = getString(postPrefix + "/Text", null);
+                       if (postAttributesAreInvalid(postTime, postText)) {
+                               throw new InvalidPostFound();
+                       }
+                       PostBuilder postBuilder = postBuilderFactory.newPostBuilder()
+                                       .withId(postId)
+                                       .from(sone.getId())
+                                       .withTime(postTime)
+                                       .withText(postText);
+                       String postRecipientId =
+                                       getString(postPrefix + "/Recipient", null);
+                       if (postRecipientIsValid(postRecipientId)) {
+                               postBuilder.to(postRecipientId);
+                       }
+                       posts.add(postBuilder.build());
+               }
+               return posts;
+       }
+
+       private boolean postAttributesAreInvalid(long postTime, String postText) {
+               return (postTime == 0) || (postText == null);
+       }
+
+       private boolean postRecipientIsValid(String postRecipientId) {
+               return (postRecipientId != null) && (postRecipientId.length() == 43);
+       }
+
+       public static class InvalidPostFound extends RuntimeException { }
+
+}
index ec59c92..87add90 100644 (file)
@@ -38,6 +38,7 @@ import java.util.concurrent.TimeUnit;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
+import net.pterodactylus.sone.core.ConfigurationSoneParser.InvalidPostFound;
 import net.pterodactylus.sone.core.Options.DefaultOption;
 import net.pterodactylus.sone.core.SoneInserter.SetInsertionDelay;
 import net.pterodactylus.sone.core.event.ImageInsertFinishedEvent;
@@ -1108,28 +1109,16 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                String lastInsertFingerprint = configuration.getStringValue(sonePrefix + "/LastInsertFingerprint").getValue("");
 
                /* load profile. */
-               Profile profile = parseProfileFromConfiguration(configuration, sone, sonePrefix);
+               ConfigurationSoneParser configurationSoneParser = new ConfigurationSoneParser(configuration, sone);
+               Profile profile = configurationSoneParser.parseProfile();
 
                /* load posts. */
-               Set<Post> posts = new HashSet<Post>();
-               while (true) {
-                       String postPrefix = sonePrefix + "/Posts/" + posts.size();
-                       String postId = configuration.getStringValue(postPrefix + "/ID").getValue(null);
-                       if (postId == null) {
-                               break;
-                       }
-                       String postRecipientId = configuration.getStringValue(postPrefix + "/Recipient").getValue(null);
-                       long postTime = configuration.getLongValue(postPrefix + "/Time").getValue((long) 0);
-                       String postText = configuration.getStringValue(postPrefix + "/Text").getValue(null);
-                       if ((postTime == 0) || (postText == null)) {
-                               logger.log(Level.WARNING, "Invalid post found, aborting load!");
-                               return;
-                       }
-                       PostBuilder postBuilder = postBuilder().withId(postId).from(sone.getId()).withTime(postTime).withText(postText);
-                       if ((postRecipientId != null) && (postRecipientId.length() == 43)) {
-                               postBuilder.to(postRecipientId);
-                       }
-                       posts.add(postBuilder.build());
+               Collection<Post> posts;
+               try {
+                       posts = configurationSoneParser.parsePosts(database);
+               } catch (InvalidPostFound ipf) {
+                       logger.log(Level.WARNING, "Invalid post found, aborting load!");
+                       return;
                }
 
                /* load replies. */
@@ -1291,29 +1280,6 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                logger.info(String.format("Sone loaded successfully: %s", sone));
        }
 
-       private static Profile parseProfileFromConfiguration(Configuration configuration, Sone sone, String sonePrefix) {
-               Profile profile = new Profile(sone);
-               profile.setFirstName(configuration.getStringValue(sonePrefix + "/Profile/FirstName").getValue(null));
-               profile.setMiddleName(configuration.getStringValue(sonePrefix + "/Profile/MiddleName").getValue(null));
-               profile.setLastName(configuration.getStringValue(sonePrefix + "/Profile/LastName").getValue(null));
-               profile.setBirthDay(configuration.getIntValue(sonePrefix + "/Profile/BirthDay").getValue(null));
-               profile.setBirthMonth(configuration.getIntValue(sonePrefix + "/Profile/BirthMonth").getValue(null));
-               profile.setBirthYear(configuration.getIntValue(sonePrefix + "/Profile/BirthYear").getValue(null));
-
-               /* load profile fields. */
-               while (true) {
-                       String fieldPrefix = sonePrefix + "/Profile/Fields/" + profile.getFields().size();
-                       String fieldName = configuration.getStringValue(fieldPrefix + "/Name").getValue(null);
-                       if (fieldName == null) {
-                               break;
-                       }
-                       String fieldValue = configuration.getStringValue(fieldPrefix + "/Value").getValue("");
-                       profile.addField(fieldName).setValue(fieldValue);
-               }
-
-               return profile;
-       }
-
        /**
         * Creates a new post.
         *
index cdafcca..14a8a00 100644 (file)
 
 package net.pterodactylus.sone;
 
-import static com.google.common.base.Objects.equal;
-import static com.google.common.collect.Iterators.size;
-import static java.util.Arrays.asList;
 import static java.util.regex.Pattern.compile;
 
 import java.io.IOException;
 import java.io.InputStream;
-import java.util.Collection;
-import java.util.Iterator;
-import java.util.List;
 
-import freenet.support.SimpleFieldSet;
+import net.pterodactylus.sone.data.Post;
 
-import com.google.common.base.Objects;
-import com.google.common.collect.Lists;
+import com.google.common.base.Optional;
 import org.hamcrest.Description;
 import org.hamcrest.Matcher;
+import org.hamcrest.TypeSafeDiagnosingMatcher;
 import org.hamcrest.TypeSafeMatcher;
 
 /**
@@ -89,10 +83,83 @@ public class Matchers {
                        }
 
                        @Override
-                       protected void describeMismatchSafely(InputStream item, Description mismatchDescription) {
+                       protected void describeMismatchSafely(InputStream item,
+                                       Description mismatchDescription) {
                                mismatchDescription.appendValue(readData);
                        }
                };
        }
 
+       public static Matcher<Post> isPost(String postId, long time,
+                       String text, Optional<String> recipient) {
+               return new PostMatcher(postId, time, text, recipient);
+       }
+
+       private static class PostMatcher extends TypeSafeDiagnosingMatcher<Post> {
+
+               private final String postId;
+               private final long time;
+               private final String text;
+               private final Optional<String> recipient;
+
+               private PostMatcher(String postId, long time, String text,
+                               Optional<String> recipient) {
+                       this.postId = postId;
+                       this.time = time;
+                       this.text = text;
+                       this.recipient = recipient;
+               }
+
+               @Override
+               protected boolean matchesSafely(Post post,
+                               Description mismatchDescription) {
+                       if (!post.getId().equals(postId)) {
+                               mismatchDescription.appendText("ID is not ")
+                                               .appendValue(postId);
+                               return false;
+                       }
+                       if (post.getTime() != time) {
+                               mismatchDescription.appendText("Time is not @")
+                                               .appendValue(time);
+                               return false;
+                       }
+                       if (!post.getText().equals(text)) {
+                               mismatchDescription.appendText("Text is not ")
+                                               .appendValue(text);
+                               return false;
+                       }
+                       if (recipient.isPresent()) {
+                               if (!post.getRecipientId().isPresent()) {
+                                       mismatchDescription.appendText(
+                                                       "Recipient not present");
+                                       return false;
+                               }
+                               if (!post.getRecipientId().get().equals(recipient.get())) {
+                                       mismatchDescription.appendText("Recipient is not ")
+                                                       .appendValue(recipient.get());
+                                       return false;
+                               }
+                       } else {
+                               if (post.getRecipientId().isPresent()) {
+                                       mismatchDescription.appendText("Recipient is present");
+                                       return false;
+                               }
+                       }
+                       return true;
+               }
+
+               @Override
+               public void describeTo(Description description) {
+                       description.appendText("is post with ID ")
+                                       .appendValue(postId);
+                       description.appendText(", created at @").appendValue(time);
+                       description.appendText(", text ").appendValue(text);
+                       if (recipient.isPresent()) {
+                               description.appendText(", directed at ")
+                                               .appendValue(recipient.get());
+                       }
+               }
+
+       }
+
 }
diff --git a/src/test/java/net/pterodactylus/sone/core/ConfigurationSoneParserTest.java b/src/test/java/net/pterodactylus/sone/core/ConfigurationSoneParserTest.java
new file mode 100644 (file)
index 0000000..daf2cf9
--- /dev/null
@@ -0,0 +1,292 @@
+package net.pterodactylus.sone.core;
+
+import static com.google.common.base.Optional.fromNullable;
+import static com.google.common.base.Optional.of;
+import static java.lang.System.currentTimeMillis;
+import static java.util.UUID.randomUUID;
+import static net.pterodactylus.sone.Matchers.isPost;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.emptyIterable;
+import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.hamcrest.Matchers.nullValue;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+import net.pterodactylus.sone.core.ConfigurationSoneParser.InvalidPostFound;
+import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.sone.data.Profile;
+import net.pterodactylus.sone.data.Profile.Field;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.database.PostBuilder;
+import net.pterodactylus.sone.database.PostBuilderFactory;
+import net.pterodactylus.util.config.Configuration;
+import net.pterodactylus.util.config.ConfigurationException;
+import net.pterodactylus.util.config.Value;
+
+import com.google.common.base.Optional;
+import org.hamcrest.Matchers;
+import org.junit.Test;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+/**
+ * Unit test for {@link ConfigurationSoneParser}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class ConfigurationSoneParserTest {
+
+       private final Configuration configuration = mock(Configuration.class);
+       private final Sone sone = mock(Sone.class);
+       private final ConfigurationSoneParser configurationSoneParser;
+
+       public ConfigurationSoneParserTest() {
+               when(sone.getId()).thenReturn("1");
+               configurationSoneParser =
+                               new ConfigurationSoneParser(configuration, sone);
+       }
+
+       @Test
+       public void emptyProfileIsLoadedCorrectly() {
+               setupEmptyProfile();
+               Profile profile = configurationSoneParser.parseProfile();
+               assertThat(profile, notNullValue());
+               assertThat(profile.getFirstName(), nullValue());
+               assertThat(profile.getMiddleName(), nullValue());
+               assertThat(profile.getLastName(), nullValue());
+               assertThat(profile.getBirthDay(), nullValue());
+               assertThat(profile.getBirthMonth(), nullValue());
+               assertThat(profile.getBirthYear(), nullValue());
+               assertThat(profile.getFields(), emptyIterable());
+       }
+
+       private void setupEmptyProfile() {
+               when(configuration.getStringValue(anyString())).thenReturn(
+                               new TestValue<String>(null));
+               when(configuration.getIntValue(anyString())).thenReturn(
+                               new TestValue<Integer>(null));
+       }
+
+       @Test
+       public void filledProfileWithFieldsIsParsedCorrectly() {
+               setupFilledProfile();
+               Profile profile = configurationSoneParser.parseProfile();
+               assertThat(profile, notNullValue());
+               assertThat(profile.getFirstName(), is("First"));
+               assertThat(profile.getMiddleName(), is("M."));
+               assertThat(profile.getLastName(), is("Last"));
+               assertThat(profile.getBirthDay(), is(18));
+               assertThat(profile.getBirthMonth(), is(12));
+               assertThat(profile.getBirthYear(), is(1976));
+               final List<Field> fields = profile.getFields();
+               assertThat(fields, hasSize(2));
+               assertThat(fields.get(0).getName(), is("Field1"));
+               assertThat(fields.get(0).getValue(), is("Value1"));
+               assertThat(fields.get(1).getName(), is("Field2"));
+               assertThat(fields.get(1).getValue(), is("Value2"));
+       }
+
+       private void setupFilledProfile() {
+               setupString("Sone/1/Profile/FirstName", "First");
+               setupString("Sone/1/Profile/MiddleName", "M.");
+               setupString("Sone/1/Profile/LastName", "Last");
+               setupInteger("Sone/1/Profile/BirthDay", 18);
+               setupInteger("Sone/1/Profile/BirthMonth", 12);
+               setupInteger("Sone/1/Profile/BirthYear", 1976);
+               setupString("Sone/1/Profile/Fields/0/Name", "Field1");
+               setupString("Sone/1/Profile/Fields/0/Value", "Value1");
+               setupString("Sone/1/Profile/Fields/1/Name", "Field2");
+               setupString("Sone/1/Profile/Fields/1/Value", "Value2");
+               setupString("Sone/1/Profile/Fields/2/Name", null);
+       }
+
+       private void setupString(String nodeName, String value) {
+               when(configuration.getStringValue(eq(nodeName))).thenReturn(
+                               new TestValue<String>(value));
+       }
+
+       private void setupInteger(String nodeName, Integer value) {
+               when(configuration.getIntValue(eq(nodeName))).thenReturn(
+                               new TestValue<Integer>(value));
+       }
+
+       @Test
+       public void postsAreParsedCorrectly() {
+               setupCompletePosts();
+               PostBuilderFactory postBuilderFactory = createPostBuilderFactory();
+               Collection<Post> posts =
+                               configurationSoneParser.parsePosts(postBuilderFactory);
+               assertThat(posts,
+                               Matchers.<Post>containsInAnyOrder(
+                                               isPost("P0", 1000L, "T0", Optional.<String>absent()),
+                                               isPost("P1", 1001L, "T1",
+                                                               of("1234567890123456789012345678901234567890123"))));
+       }
+
+       private PostBuilderFactory createPostBuilderFactory() {
+               PostBuilderFactory postBuilderFactory =
+                               mock(PostBuilderFactory.class);
+               when(postBuilderFactory.newPostBuilder()).thenAnswer(
+                               new Answer<PostBuilder>() {
+                                       @Override
+                                       public PostBuilder answer(InvocationOnMock invocation)
+                                       throws Throwable {
+                                               return new TestPostBuilder();
+                                       }
+                               });
+               return postBuilderFactory;
+       }
+
+       private void setupCompletePosts() {
+               setupPost("0", "P0", 1000L, "T0", null);
+               setupPost("1", "P1", 1001L, "T1",
+                               "1234567890123456789012345678901234567890123");
+               setupPost("2", null, 0L, null, null);
+       }
+
+       private void setupPost(String postNumber, String postId, long time,
+                       String text, String recipientId) {
+               setupString("Sone/1/Posts/" + postNumber + "/ID", postId);
+               setupLong("Sone/1/Posts/" + postNumber + "/Time", time);
+               setupString("Sone/1/Posts/" + postNumber + "/Text", text);
+               setupString("Sone/1/Posts/" + postNumber + "/Recipient", recipientId);
+       }
+
+       private void setupLong(String nodeName, Long value) {
+               when(configuration.getLongValue(eq(nodeName))).thenReturn(
+                               new TestValue<Long>(value));
+       }
+
+       @Test(expected = InvalidPostFound.class)
+       public void postWithoutTimeIsRecognized() {
+               setupPostWithoutTime();
+               configurationSoneParser.parsePosts(createPostBuilderFactory());
+       }
+
+       private void setupPostWithoutTime() {
+               setupPost("0", "P0", 0L, "T0", null);
+       }
+
+       @Test(expected = InvalidPostFound.class)
+       public void postWithoutTextIsRecognized() {
+               setupPostWithoutText();
+               configurationSoneParser.parsePosts(createPostBuilderFactory());
+       }
+
+       private void setupPostWithoutText() {
+               setupPost("0", "P0", 1000L, null, null);
+       }
+
+       @Test
+       public void postWithInvalidRecipientIdIsRecognized() {
+               setupPostWithInvalidRecipientId();
+               Collection<Post> posts = configurationSoneParser.parsePosts(
+                               createPostBuilderFactory());
+               assertThat(posts, contains(
+                               isPost("P0", 1000L, "T0", Optional.<String>absent())));
+       }
+
+       private void setupPostWithInvalidRecipientId() {
+               setupPost("0", "P0", 1000L, "T0", "123");
+               setupPost("1", null, 0L, null, null);
+       }
+
+
+       private static class TestValue<T> implements Value<T> {
+
+               private final AtomicReference<T> value = new AtomicReference<T>();
+
+               public TestValue(T originalValue) {
+                       value.set(originalValue);
+               }
+
+               @Override
+               public T getValue() throws ConfigurationException {
+                       return value.get();
+               }
+
+               @Override
+               public T getValue(T defaultValue) {
+                       final T realValue = value.get();
+                       return (realValue != null) ? realValue : defaultValue;
+               }
+
+               @Override
+               public void setValue(T newValue) throws ConfigurationException {
+                       value.set(newValue);
+               }
+
+       }
+
+       private static class TestPostBuilder implements PostBuilder {
+
+               private final Post post = mock(Post.class);
+               private String recipientId = null;
+
+               @Override
+               public PostBuilder copyPost(Post post) throws NullPointerException {
+                       return this;
+               }
+
+               @Override
+               public PostBuilder from(String senderId) {
+                       final Sone sone = mock(Sone.class);
+                       when(sone.getId()).thenReturn(senderId);
+                       when(post.getSone()).thenReturn(sone);
+                       return this;
+               }
+
+               @Override
+               public PostBuilder randomId() {
+                       when(post.getId()).thenReturn(randomUUID().toString());
+                       return this;
+               }
+
+               @Override
+               public PostBuilder withId(String id) {
+                       when(post.getId()).thenReturn(id);
+                       return this;
+               }
+
+               @Override
+               public PostBuilder currentTime() {
+                       when(post.getTime()).thenReturn(currentTimeMillis());
+                       return this;
+               }
+
+               @Override
+               public PostBuilder withTime(long time) {
+                       when(post.getTime()).thenReturn(time);
+                       return this;
+               }
+
+               @Override
+               public PostBuilder withText(String text) {
+                       when(post.getText()).thenReturn(text);
+                       return this;
+               }
+
+               @Override
+               public PostBuilder to(String recipientId) {
+                       this.recipientId = recipientId;
+                       return this;
+               }
+
+               @Override
+               public Post build() throws IllegalStateException {
+                       when(post.getRecipientId()).thenReturn(fromNullable(recipientId));
+                       return post;
+               }
+
+       }
+
+}