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