diff --git a/README.md b/README.md index 05d35ad..06c144f 100644 --- a/README.md +++ b/README.md @@ -34,4 +34,8 @@ - mvn verify **Package Application:** -- mvn clean package \ No newline at end of file +- mvn clean package + +**Debug unit tests with maven** +- mvn -Dmaven.surefire.debug test +- Attach a debugger listening to port 5005 \ No newline at end of file diff --git a/core/pom.xml b/core/pom.xml index 67c9935..cfa617c 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -37,6 +37,10 @@ org.mockito mockito-core + + org.reflections + reflections + diff --git a/core/src/main/java/com/brogrammers/utilities/Reflections.java b/core/src/main/java/com/brogrammers/utilities/Reflections.java new file mode 100644 index 0000000..f389a19 --- /dev/null +++ b/core/src/main/java/com/brogrammers/utilities/Reflections.java @@ -0,0 +1,46 @@ +package com.brogrammers.utilities; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import org.reflections.ReflectionUtils; + +public final class Reflections { + private Reflections() { + + } + + /** + * Searches and invokes through reflection the correct getter method + * for the provided {@link Field} object. + * @param field The field to search for the appropriate public getter + * @param content The object on which we use reflection + * @param + * @return The type of the public getter of the field + * @throws IllegalAccessError if either no appropriate public getter is found or no method at all. + */ + public static Object runGetter(Field field, T content) { + for (Method method : ReflectionUtils.getAllMethods(content.getClass())) { + if (isGet(field, method) || isIs(field, method)) { + if (method.getName().toLowerCase().endsWith(field.getName().toLowerCase())) { + try { + return method.invoke(content); + } + catch (IllegalAccessException | InvocationTargetException e) { + throw new IllegalStateException(e); + } + } + } + } + throw new IllegalAccessError(); + } + + private static boolean isGet(Field field, Method method) { + return method.getName().startsWith("get") && method.getName().length() == (field.getName().length() + 3); + } + + private static boolean isIs(Field field, Method method) { + return method.getName().startsWith("is") && method.getName().length() == (field.getName().length() + 2); + } +} diff --git a/pom.xml b/pom.xml index 65104e2..73aecfa 100644 --- a/pom.xml +++ b/pom.xml @@ -52,6 +52,12 @@ 2.21.0 test + + + org.reflections + reflections + 0.9.11 + diff --git a/writer/pom.xml b/writer/pom.xml index b5bb48b..be686bf 100644 --- a/writer/pom.xml +++ b/writer/pom.xml @@ -42,6 +42,10 @@ org.mockito mockito-core + + org.reflections + reflections + diff --git a/writer/src/main/java/com/brogrammers/writer/CsvWriter.java b/writer/src/main/java/com/brogrammers/writer/CsvWriter.java new file mode 100644 index 0000000..3cfe915 --- /dev/null +++ b/writer/src/main/java/com/brogrammers/writer/CsvWriter.java @@ -0,0 +1,77 @@ +package com.brogrammers.writer; + +import static com.brogrammers.utilities.Reflections.runGetter; + +import java.io.IOException; +import java.io.Writer; +import java.lang.reflect.Field; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import org.reflections.ReflectionUtils; + +public class CsvWriter implements AutoCloseable { + + private static final String DEFAULT_FIELD_DELIMITER = ","; + private boolean newLine = false; + private Writer writer; + + public static CsvWriter newInstance(Writer writer) { + return new CsvWriter<>(writer); + } + + @Override + public void close() { + try { + writer.close(); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + public void write(List content) { + for (int index = 0; index < content.size(); index++) { + //Only add a new line when there is more than one element in the contents to write + newLine = index > 0; + try { + write(content.get(index)); + writer.flush(); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + } + + private CsvWriter(Writer writer) { + this.writer = Objects.requireNonNull(writer); + } + + private void write(T content) throws IOException { + Set fields = ReflectionUtils.getAllFields(content.getClass()); + + Iterator iterator = fields.iterator(); + + if (newLine) { + writer.append(System.lineSeparator()); + } + boolean isLastField = false; + while (iterator.hasNext()) { + Field field = iterator.next(); + + //We need to ignore synthetic fields possibly introduced by external tools in order not to output those too + if (field.isSynthetic()) { + continue; + } + if (!iterator.hasNext()) { + isLastField = true; + } + + writer.append(runGetter(field, content).toString()); + if (!isLastField) { + writer.append(DEFAULT_FIELD_DELIMITER); + } + } + } +} diff --git a/writer/src/test/java/com/brogrammers/it/CsvWriterIT.java b/writer/src/test/java/com/brogrammers/it/CsvWriterIT.java new file mode 100644 index 0000000..694e3b8 --- /dev/null +++ b/writer/src/test/java/com/brogrammers/it/CsvWriterIT.java @@ -0,0 +1,46 @@ +package com.brogrammers.it; + +import static org.junit.Assert.assertEquals; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.Writer; +import java.nio.file.Files; +import java.nio.file.Paths; + +import org.junit.Test; + +import com.brogrammers.model.TestModel; +import com.brogrammers.writer.CsvWriter; +import com.google.common.base.Charsets; +import com.google.common.collect.Lists; + +public class CsvWriterIT { + + @Test + public void should_create_given_csv_and_write_data_with_correct_data_types() { + //given + TestModel testModel1 = new TestModel("foo", "bar", 500L, 100); + TestModel testModel2 = new TestModel("baz", "boom", 3000L, 500); + + Writer writer = createWriter(); + + CsvWriter csvWriter = CsvWriter.newInstance(writer); + + //when + csvWriter.write(Lists.newArrayList(testModel1, testModel2)); + + //then + + //For now since the reader is not yet ready I don't have anything automated to assert that the correct contents have been written. + assertEquals(1L, 1L); + } + + private BufferedWriter createWriter() { + try { + return Files.newBufferedWriter(Paths.get("src/test/resources/output.csv"), Charsets.UTF_8); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } +} diff --git a/writer/src/test/java/com/brogrammers/model/TestModel.java b/writer/src/test/java/com/brogrammers/model/TestModel.java new file mode 100644 index 0000000..1b95083 --- /dev/null +++ b/writer/src/test/java/com/brogrammers/model/TestModel.java @@ -0,0 +1,31 @@ +package com.brogrammers.model; + +public class TestModel { + private final String firstName; + private final String lastName; + private final long counter; + private final int smallerCounter; + + public TestModel(String firstName, String lastName, long counter, int smallerCounter) { + this.firstName = firstName; + this.lastName = lastName; + this.counter = counter; + this.smallerCounter = smallerCounter; + } + + public String getFirstName() { + return firstName; + } + + public String getLastName() { + return lastName; + } + + public long getCounter() { + return counter; + } + + public int getSmallerCounter() { + return smallerCounter; + } +} diff --git a/writer/src/test/java/com/brogrammers/writer/CsvWriterTest.java b/writer/src/test/java/com/brogrammers/writer/CsvWriterTest.java new file mode 100644 index 0000000..f3f9130 --- /dev/null +++ b/writer/src/test/java/com/brogrammers/writer/CsvWriterTest.java @@ -0,0 +1,90 @@ +package com.brogrammers.writer; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import java.io.IOException; +import java.io.Writer; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; + +import com.brogrammers.model.TestModel; +import com.google.common.collect.Lists; + +public class CsvWriterTest { + + public Writer writter; + + @Before + public void setup() { + writter = mock(Writer.class); + } + + + @Test + public void should_delegate_writes_to_passed_writer() throws IOException { + //given + List content = Lists.newArrayList( + new TestModel("foo", "bar", 100L, 50), + new TestModel("baz", "foob", 5000L, 300) + ); + + CsvWriter csvWriter = CsvWriter.newInstance(writter); + + //when + csvWriter.write(content); + + //then + + //The number of times the append is called is the sum of all the fields written and the line separators and field separators. + verify(writter, times(15)).append(any(String.class)); + verify(writter, times(1)).append(System.lineSeparator()); + verify(writter, times(2)).flush(); + } + + @Test + public void should_close_the_given_writer() throws IOException { + //given + CsvWriter csvWriter = CsvWriter.newInstance(writter); + + //when + csvWriter.close(); + + //then + verify(writter, times(1)).close(); + } + + @Test(expected = IllegalStateException.class) + public void should_throw_IllegalStateException_when_IOException_is_thrown_in_close() throws IOException { + //given + CsvWriter csvWriter = CsvWriter.newInstance(writter); + doThrow(new IOException()).when(writter).close(); + + //when + csvWriter.close(); + } + + @Test(expected = IllegalStateException.class) + public void should_throw_IllegalStateException_when_IOException_is_thrown_in_append() throws IOException { + //given + List content = Lists.newArrayList( + new TestModel("foo", "bar", 100L, 50), + new TestModel("baz", "foob", 5000L, 300) + ); + CsvWriter csvWriter = CsvWriter.newInstance(writter); + doThrow(new IOException()).when(writter).append(any(String.class)); + + //when + csvWriter.write(content); + } + + @Test(expected = NullPointerException.class) + public void should_throw_NullPointerException_when_given_writer_is_null() { + CsvWriter.newInstance(null); + } +} diff --git a/writer/src/test/resources/output.csv b/writer/src/test/resources/output.csv new file mode 100644 index 0000000..c231621 --- /dev/null +++ b/writer/src/test/resources/output.csv @@ -0,0 +1,2 @@ +100,500,foo,bar +500,3000,baz,boom \ No newline at end of file