From 11f3457415c9122b5d11840d24186971af28add9 Mon Sep 17 00:00:00 2001 From: =?utf8?q?David=20=E2=80=98Bombe=E2=80=99=20Roden?= Date: Sun, 7 Sep 2014 19:16:27 +0200 Subject: [PATCH] Start moving parsing a Sone from a configuration to a specialized parser. --- .../sone/core/ConfigurationSoneParser.java | 111 ++++++++ .../java/net/pterodactylus/sone/core/Core.java | 52 +--- src/test/java/net/pterodactylus/sone/Matchers.java | 87 +++++- .../sone/core/ConfigurationSoneParserTest.java | 292 +++++++++++++++++++++ 4 files changed, 489 insertions(+), 53 deletions(-) create mode 100644 src/main/java/net/pterodactylus/sone/core/ConfigurationSoneParser.java create mode 100644 src/test/java/net/pterodactylus/sone/core/ConfigurationSoneParserTest.java 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 index 0000000..8f09b3b --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/core/ConfigurationSoneParser.java @@ -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 David ‘Bombe’ Roden + */ +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 parsePosts(PostBuilderFactory postBuilderFactory) + throws InvalidPostFound { + Set posts = new HashSet(); + 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 { } + +} diff --git a/src/main/java/net/pterodactylus/sone/core/Core.java b/src/main/java/net/pterodactylus/sone/core/Core.java index ec59c92..87add90 100644 --- a/src/main/java/net/pterodactylus/sone/core/Core.java +++ b/src/main/java/net/pterodactylus/sone/core/Core.java @@ -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 posts = new HashSet(); - 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 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. * diff --git a/src/test/java/net/pterodactylus/sone/Matchers.java b/src/test/java/net/pterodactylus/sone/Matchers.java index cdafcca..14a8a00 100644 --- a/src/test/java/net/pterodactylus/sone/Matchers.java +++ b/src/test/java/net/pterodactylus/sone/Matchers.java @@ -17,23 +17,17 @@ 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 isPost(String postId, long time, + String text, Optional recipient) { + return new PostMatcher(postId, time, text, recipient); + } + + private static class PostMatcher extends TypeSafeDiagnosingMatcher { + + private final String postId; + private final long time; + private final String text; + private final Optional recipient; + + private PostMatcher(String postId, long time, String text, + Optional 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 index 0000000..daf2cf9 --- /dev/null +++ b/src/test/java/net/pterodactylus/sone/core/ConfigurationSoneParserTest.java @@ -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 David ‘Bombe’ Roden + */ +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(null)); + when(configuration.getIntValue(anyString())).thenReturn( + new TestValue(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 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(value)); + } + + private void setupInteger(String nodeName, Integer value) { + when(configuration.getIntValue(eq(nodeName))).thenReturn( + new TestValue(value)); + } + + @Test + public void postsAreParsedCorrectly() { + setupCompletePosts(); + PostBuilderFactory postBuilderFactory = createPostBuilderFactory(); + Collection posts = + configurationSoneParser.parsePosts(postBuilderFactory); + assertThat(posts, + Matchers.containsInAnyOrder( + isPost("P0", 1000L, "T0", Optional.absent()), + isPost("P1", 1001L, "T1", + of("1234567890123456789012345678901234567890123")))); + } + + private PostBuilderFactory createPostBuilderFactory() { + PostBuilderFactory postBuilderFactory = + mock(PostBuilderFactory.class); + when(postBuilderFactory.newPostBuilder()).thenAnswer( + new Answer() { + @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(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 posts = configurationSoneParser.parsePosts( + createPostBuilderFactory()); + assertThat(posts, contains( + isPost("P0", 1000L, "T0", Optional.absent()))); + } + + private void setupPostWithInvalidRecipientId() { + setupPost("0", "P0", 1000L, "T0", "123"); + setupPost("1", null, 0L, null, null); + } + + + private static class TestValue implements Value { + + private final AtomicReference value = new AtomicReference(); + + 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; + } + + } + +} -- 2.7.4