score;
}
\ No newline at end of file
diff --git a/src/main/java/org/writer/service/CsvWriter.java b/src/main/java/org/writer/service/CsvWriter.java
new file mode 100644
index 0000000..c574227
--- /dev/null
+++ b/src/main/java/org/writer/service/CsvWriter.java
@@ -0,0 +1,117 @@
+package org.writer.service;
+
+import java.io.IOException;
+import java.lang.reflect.AccessibleObject;
+import java.lang.reflect.Field;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+import org.writer.Writable;
+import org.writer.annotation.CsvWritable;
+
+/**
+ * {@code CsvWriter} provides implementation of {@code @Writeable} interface
+ *
+ * CsvWriter provides method to save data in CSV format using {@code CsvWritable} annotation marks
+ *
+ */
+public class CsvWriter implements Writable {
+
+ /**
+ * Saves list of objects in CSV formatted file
+ * @param data list of values to be saved
+ * @param fileName path or name of file
+ * @throws IllegalArgumentException when list contains objects from different classes
+ * @throws RuntimeException when can not write to file or can not read object field
+ */
+ @Override
+ public void writeToFile(List> data, String fileName) {
+ if (data == null || data.isEmpty()) {
+ return;
+ }
+
+ Object obj = data.stream().filter(Objects::nonNull).findFirst().orElse(null);
+ if (obj == null) {
+ return;
+ }
+
+ Class> targetClass = obj.getClass();
+
+ boolean isClassWriteable =
+ targetClass.isAnnotationPresent(CsvWritable.class) && targetClass.getAnnotation(
+ CsvWritable.class).value();
+
+ List writeableFields = Arrays.stream(targetClass.getDeclaredFields())
+ .filter(field -> isClassWriteable || field
+ .isAnnotationPresent(CsvWritable.class) && field.getAnnotation(
+ CsvWritable.class).value()).peek(AccessibleObject::trySetAccessible).toList();
+
+ if (writeableFields.isEmpty()) {
+ return;
+ }
+
+ String result =
+ writeableFields.stream().map(Field::getName).collect(Collectors.joining(",", "", "\n"))
+ + data.stream()
+ .map(writeableObj -> {
+
+ if (!writeableObj.getClass().equals(obj.getClass())) {
+ throw new IllegalArgumentException("Object type inconsistent. Expected %s but found %s".formatted(targetClass, obj.getClass().getName()) );
+ }
+
+ return writeableFields.stream()
+ .map(field -> getFormattedValue(field, writeableObj))
+ .collect(Collectors.joining(","));
+ }
+ ).collect(Collectors.joining("\n"));
+ try {
+ String filePath = fileName.concat(fileName.endsWith(".csv") ? "" : ".csv");
+ Files.writeString(Path.of(filePath), result);
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to write CSV file: " + fileName, e);
+ }
+ }
+
+ private String getFormattedValue(Field field, Object object) {
+ Object fieldVal;
+ try {
+ fieldVal = field.get(object);
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException("Could not read field: " + field.getName(),
+ e);
+ }
+
+ if (fieldVal == null) {
+ return "";
+ }
+
+ String stringVal;
+
+ if (fieldVal instanceof Collection>) {
+ stringVal = ((Collection>) fieldVal).stream()
+ .map(item -> item == null ? "" : item.toString())
+ .collect(Collectors.joining(";"));
+ } else {
+ stringVal = String.valueOf(fieldVal);
+ }
+
+ boolean needsQuotes =
+ stringVal.contains("\n")
+ || stringVal.contains("\"")
+ || stringVal.contains(",")
+ || stringVal.contains("\r");
+
+ if (needsQuotes) {
+ stringVal = stringVal.replace("\"", "\"\"");
+ stringVal = "\"" + stringVal + "\"";
+ }
+
+ return stringVal;
+ }
+
+}
+
diff --git a/src/test/java/org/writer/service/CsvWriterTest.java b/src/test/java/org/writer/service/CsvWriterTest.java
new file mode 100644
index 0000000..237b47b
--- /dev/null
+++ b/src/test/java/org/writer/service/CsvWriterTest.java
@@ -0,0 +1,113 @@
+package org.writer.service;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Stream;
+import net.datafaker.Faker;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.writer.model.Company;
+import org.writer.model.Months;
+import org.writer.model.Person;
+import org.writer.model.Student;
+
+class CsvWriterTest {
+
+ private final Faker faker = new Faker();
+ private CsvWriter csvWriter;
+ private Path csvPath;
+
+ @BeforeEach
+ void setUp(@TempDir Path tempDir) {
+ this.csvWriter = new CsvWriter();
+ this.csvPath = tempDir.resolve("file.csv");
+
+ }
+
+ @Test
+ void writeToFile_EmptyFields() throws IOException {
+ List students = List.of(
+ new Student(null, List.of("A", "B")),
+ new Student("Vladimir", null));
+
+ List companies = List.of(
+ new Company("123PL Limited", "Field View, Castledon Road, Downham, Billericay, CM11 1LH", null),
+ new Company("2B&C Logistics Ltd", null, "08229231"));
+
+ csvWriter.writeToFile(students, csvPath.toString());
+
+ List lines = Files.readAllLines(csvPath);
+
+ assertEquals(3, lines.size());
+ assertEquals(",A;B", lines.get(1));
+ assertEquals("Vladimir,", lines.get(2));
+
+ csvWriter.writeToFile(companies, csvPath.toString());
+
+ lines = Files.readAllLines(csvPath);
+
+ assertEquals(3, lines.size());
+ assertEquals("123PL Limited,\"Field View, Castledon Road, Downham, Billericay, CM11 1LH\",", lines.get(1));
+ assertEquals("2B&C Logistics Ltd,,08229231", lines.get(2));
+
+
+ }
+
+ @Test
+ void writeToFile_ValidStructure() throws IOException {
+
+ List students = List.of(
+ new Student("Alexey", List.of("A", "B")),
+ new Student("Vladimir", List.of("D", "A", "C", "B")),
+ new Student("Daniil", Collections.emptyList()),
+ new Student("Matvey", List.of("E")));
+
+ csvWriter.writeToFile(students, csvPath.toString());
+
+ List lines = Files.readAllLines(csvPath);
+
+ assertEquals(5, lines.size());
+ assertEquals("Alexey,A;B", lines.get(1));
+ assertEquals("Vladimir,D;A;C;B", lines.get(2));
+ assertEquals("Daniil,", lines.get(3));
+ assertEquals("Matvey,E", lines.get(4));
+
+ List persons = List.of(
+ new Person("First", "Last", 12, Months.APRIL, 1999),
+ new Person("Name", "Test", 29, Months.MARCH, 2000),
+ new Person("Some", "Person", 28, Months.FEBRUARY, 1956),
+ new Person("John", "Doe", 1, Months.JANUARY, 1981)
+ );
+
+ csvWriter.writeToFile(persons, csvPath.toString());
+
+ lines = Files.readAllLines(csvPath);
+
+ assertEquals(5, lines.size());
+ assertEquals("First,12,APRIL", lines.get(1));
+ assertEquals("Name,29,MARCH", lines.get(2));
+ assertEquals("Some,28,FEBRUARY", lines.get(3));
+ assertEquals("John,1,JANUARY", lines.get(4));
+
+ List companies = List.of(
+ new Company("123PL Limited", "Field View, Castledon Road, Downham, Billericay, CM11 1LH", "07682974"),
+ new Company("2B&C Logistics Ltd", "5 Copperhouse Court, Caldecotte, Milton Keynes, MK7 8NL", "08229231"),
+ new Company("Benappi Fine Art Limited", "5th Floor 86 Jermyn Street, London, SW1Y 6AW", "08522336")
+ );
+
+ csvWriter.writeToFile(companies, csvPath.toString());
+
+ lines = Files.readAllLines(csvPath);
+
+ assertEquals(4, lines.size());
+ assertEquals("123PL Limited,\"Field View, Castledon Road, Downham, Billericay, CM11 1LH\",07682974", lines.get(1));
+ assertEquals("2B&C Logistics Ltd,\"5 Copperhouse Court, Caldecotte, Milton Keynes, MK7 8NL\",08229231", lines.get(2));
+ assertEquals("Benappi Fine Art Limited,\"5th Floor 86 Jermyn Street, London, SW1Y 6AW\",08522336", lines.get(3));
+ }
+}
\ No newline at end of file