diff --git a/pom.xml b/pom.xml
index effc1bd..7fd6b43 100644
--- a/pom.xml
+++ b/pom.xml
@@ -23,10 +23,23 @@
org.junit.jupiter
- junit-jupiter
- 5.8.1
+ junit-jupiter-api
+ 5.10.0
test
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ 5.10.0
+ test
+
+
+
+ net.datafaker
+ datafaker
+ 2.0.2
+
\ No newline at end of file
diff --git a/src/main/java/org/writer/CsvWriter.java b/src/main/java/org/writer/CsvWriter.java
new file mode 100644
index 0000000..825fc90
--- /dev/null
+++ b/src/main/java/org/writer/CsvWriter.java
@@ -0,0 +1,133 @@
+package org.writer;
+
+import org.writer.annotation.CsvColumn;
+import org.writer.annotation.CsvSerializable;
+
+import java.io.BufferedWriter;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * Реализация интерфейса {@link Writable}, который сериализует список объектов в CSV файл.
+ *
+ * Данная реализация использует рефлексию, а также кастомные аннотации ({@link CsvSerializable},
+ * {@link CsvColumn}) для валидации данных, подлежащих записи.
+ */
+public class CsvWriter implements Writable {
+
+ /**
+ * Разделитель, используемый для отделения значений в строке CSV.
+ */
+ private final static String SEPARATOR = ",";
+
+ /**
+ * Разделитель, используемый для объединения элементов списка в одну ячейку CSV.
+ */
+ private static final String LIST_ITEM_DELIM = ";";
+
+ /**
+ * Записывает список объектов в CSV файл.
+ *
+ * Сначала записывается строка заголовка, основанная на именах колонок,
+ * а затем для каждого объекта из списка записывается строка данных.
+ *
+ * @param data список объектов для записи. Класс этих объектов должен быть
+ * помечен аннотацией {@link CsvSerializable}.
+ * @param fileName имя файла, в который будут сохранены данные.
+ * @throws IllegalArgumentException если список данных null или пуст, или если
+ * класс объектов не помечен аннотацией {@link CsvSerializable}.
+ * @throws RuntimeException если возникает ошибка ввода-вывода при записи в файл.
+ */
+ @Override
+ public void writeToFile(List> data, String fileName) {
+
+ if (data == null || data.isEmpty()) {
+ throw new IllegalArgumentException("Список данных не может быть пустым.");
+ }
+
+ Class> clazz = data.get(0).getClass();
+ if (!clazz.isAnnotationPresent(CsvSerializable.class)) {
+ throw new IllegalArgumentException("Класс " + clazz.getSimpleName() + " не помечен аннотацией @CsvSerializable.");
+ }
+
+ try (BufferedWriter writer = new BufferedWriter(new FileWriter(fileName))) {
+ List fields = getCsvFields(clazz);
+
+ // Header
+ String header = fields.stream()
+ .map(this::getColumnName)
+ .collect(Collectors.joining(SEPARATOR));
+
+ writer.write(header);
+ writer.newLine();
+
+ // Body
+ String body = data.stream()
+ .map(object -> fields.stream()
+ .map(field -> getFieldValue(field, object))
+ .collect(Collectors.joining(SEPARATOR)))
+ .collect(Collectors.joining(System.lineSeparator()));
+
+ writer.write(body);
+ }
+ catch (IOException e) {
+ throw new RuntimeException("Ошибка записи в файл " + fileName, e);
+ }
+ }
+
+ /**
+ * Сканирует класс на наличие полей, аннотированных {@link CsvColumn}, с помощью рефлексии.
+ *
+ * @param clazz класс для сканирования.
+ * @return список объектов {@link Field}, которые помечены для сериализации в CSV.
+ */
+ private List getCsvFields(Class> clazz){
+ return Stream.of(clazz.getDeclaredFields())
+ .filter(field -> field.isAnnotationPresent(CsvColumn.class))
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Определяет имя колонки для заданного поля.
+ *
+ * В приоритете используется атрибут {@code name} из аннотации {@link CsvColumn}.
+ * Если он пуст, используется фактическое имя поля.
+ *
+ * @param field поле, для которого нужно получить имя колонки.
+ * @return имя колонки CSV.
+ */
+ private String getColumnName(Field field) {
+ String name = field.getAnnotation(CsvColumn.class).name();
+ return name.isEmpty() ? field.getName() : name;
+ }
+
+ /**
+ * Извлекает строковое представление значения поля из заданного объекта.
+ *
+ * Предоставляет специальную обработку для полей типа {@link List},
+ * объединяя их элементы в одну строку с помощью разделителя {@code LIST_ITEM_DELIM}.
+ *
+ * @param field поле, значение которого нужно извлечь.
+ * @param object экземпляр объекта, из которого извлекается значение.
+ * @return строковое представление значения поля.
+ * @throws RuntimeException если доступ к полю невозможен.
+ */
+ private String getFieldValue(Field field, Object object) {
+ try {
+ field.setAccessible(true);
+ Object value = field.get(object);
+ if (value instanceof List) {
+ return ((List>) value).stream()
+ .map(String::valueOf)
+ .collect(Collectors.joining(LIST_ITEM_DELIM));
+ }
+ return String.valueOf(value);
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException("Не удалось получить доступ к полю " + field.getName(), e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/writer/Main.java b/src/main/java/org/writer/Main.java
index 40e8213..0fa7dec 100644
--- a/src/main/java/org/writer/Main.java
+++ b/src/main/java/org/writer/Main.java
@@ -1,7 +1,47 @@
package org.writer;
+import net.datafaker.Faker;
+import org.writer.model.Months;
+import org.writer.model.Person;
+import org.writer.model.Student;
+
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
public class Main {
public static void main(String[] args) {
- System.out.println("Hello world!");
+
+ Faker faker = new Faker();
+ Writable csvWriter = new CsvWriter();
+
+
+ List people = IntStream.range(0, 10)
+ .mapToObj(i -> Person.builder()
+ .firstName(faker.name().firstName())
+ .lastName(faker.name().lastName())
+ .dayOfBirth(faker.number().numberBetween(1, 28))
+ .monthOfBirth(Months.values()[faker.number().numberBetween(0, 11)])
+ .yearOfBirth(faker.number().numberBetween(1990, 2010))
+ .build())
+ .collect(Collectors.toList());
+
+ csvWriter.writeToFile(people, "people_report.csv");
+ System.out.println("Отчет по людям сохранен в people_report.csv");
+
+
+ List students = IntStream.range(0, 5)
+ .mapToObj(i -> Student.builder()
+ .name(faker.name().fullName())
+ .score(List.of(
+ String.valueOf(faker.number().numberBetween(1, 5)),
+ String.valueOf(faker.number().numberBetween(1, 5)),
+ String.valueOf(faker.number().numberBetween(1, 5))
+ ))
+ .build())
+ .collect(Collectors.toList());
+
+ csvWriter.writeToFile(students, "students_report.csv");
+ System.out.println("Отчет по студентам сохранен в students_report.csv");
}
}
\ No newline at end of file
diff --git a/src/main/java/org/writer/annotation/CsvColumn.java b/src/main/java/org/writer/annotation/CsvColumn.java
new file mode 100644
index 0000000..33392ad
--- /dev/null
+++ b/src/main/java/org/writer/annotation/CsvColumn.java
@@ -0,0 +1,19 @@
+package org.writer.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Указывает, что поле класса должно быть включено в CSV файл как колонка
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.FIELD)
+public @interface CsvColumn {
+ /**
+ * Имя колонки в CSV файле. Если не указано, то будет использовано имя поля.
+ * @return имя колонки
+ */
+ String name() default "";
+}
diff --git a/src/main/java/org/writer/annotation/CsvSerializable.java b/src/main/java/org/writer/annotation/CsvSerializable.java
new file mode 100644
index 0000000..68e28c1
--- /dev/null
+++ b/src/main/java/org/writer/annotation/CsvSerializable.java
@@ -0,0 +1,15 @@
+package org.writer.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Метка для класса, который должен быть сериализован в CSV формат.
+ * CsvWriter будет обрабатывать только классы, помеченные этой аннотацией.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface CsvSerializable {
+}
diff --git a/src/main/java/org/writer/model/Person.java b/src/main/java/org/writer/model/Person.java
index ba8576b..7067692 100644
--- a/src/main/java/org/writer/model/Person.java
+++ b/src/main/java/org/writer/model/Person.java
@@ -3,20 +3,27 @@
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
+import org.writer.annotation.CsvColumn;
+import org.writer.annotation.CsvSerializable;
@Data
@Builder
@AllArgsConstructor
+@CsvSerializable
public class Person {
+ @CsvColumn(name = "Имя")
private String firstName;
+ @CsvColumn(name = "Фамилия")
private String lastName;
+ @CsvColumn(name = "День рождения")
private int dayOfBirth;
+ @CsvColumn(name = "Месяц рождения")
private Months monthOfBirth;
+ @CsvColumn(name = "Год рождения")
private int yearOfBirth;
-
}
diff --git a/src/main/java/org/writer/model/Student.java b/src/main/java/org/writer/model/Student.java
index 2b549c4..e71d99b 100644
--- a/src/main/java/org/writer/model/Student.java
+++ b/src/main/java/org/writer/model/Student.java
@@ -3,15 +3,20 @@
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
+import org.writer.annotation.CsvColumn;
+import org.writer.annotation.CsvSerializable;
import java.util.List;
@Data
@Builder
@AllArgsConstructor
+@CsvSerializable
public class Student {
+ @CsvColumn(name = "Имя")
private String name;
+ @CsvColumn(name = "Оценки")
private List score;
}
\ No newline at end of file
diff --git a/src/test/java/org/writer/CsvWriterTest.java b/src/test/java/org/writer/CsvWriterTest.java
new file mode 100644
index 0000000..e9412e5
--- /dev/null
+++ b/src/test/java/org/writer/CsvWriterTest.java
@@ -0,0 +1,106 @@
+package org.writer;
+
+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.Months;
+import org.writer.model.Person;
+import org.writer.model.Student;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.stream.IntStream;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class CsvWriterTest {
+
+ private CsvWriter csvWriter;
+ private Faker faker;
+
+ @TempDir
+ Path tempDir;
+
+ @BeforeEach
+ void setUp() {
+ csvWriter = new CsvWriter();
+ faker = new Faker();
+ }
+
+ @Test
+ void testWritePersonToFile() throws IOException {
+ List persons = IntStream.range(0, 9)
+ .mapToObj(i -> Person.builder()
+ .firstName(faker.name().firstName())
+ .lastName(faker.name().lastName())
+ .dayOfBirth(faker.number().numberBetween(1, 28))
+ .monthOfBirth(Months.values()[faker.number().numberBetween(0, 11)])
+ .yearOfBirth(faker.number().numberBetween(1985, 2005))
+ .build())
+ .toList();
+
+ Path filePath = tempDir.resolve("persons.csv");
+ csvWriter.writeToFile(persons, filePath.toString());
+
+ assertTrue(Files.exists(filePath));
+
+ List lines = Files.readAllLines(filePath);
+ assertEquals(10, lines.size());
+ assertEquals("Имя,Фамилия,День рождения,Месяц рождения,Год рождения", lines.get(0));
+
+ String[] firstPersonData = lines.get(1).split(",");
+ assertEquals(persons.get(0).getFirstName(), firstPersonData[0]);
+ assertEquals(persons.get(0).getLastName(), firstPersonData[1]);
+ }
+
+ @Test
+ void testWriteStudentsToFile() throws IOException {
+ List students = IntStream.range(0, 4)
+ .mapToObj(i -> Student.builder()
+ .name(faker.name().fullName())
+ .score(List.of(
+ String.valueOf(faker.number().numberBetween(1, 5)),
+ String.valueOf(faker.number().numberBetween(1, 5))
+ )).build())
+ .toList();
+
+ Path filePath = tempDir.resolve("students.csv");
+ csvWriter.writeToFile(students, filePath.toString());
+
+ assertTrue(Files.exists(filePath));
+
+ List lines = Files.readAllLines(filePath);
+ assertEquals(5, lines.size());
+ assertEquals("Имя,Оценки", lines.get(0));
+
+ String[] firstStudentData = lines.get(1).split(",");
+ assertEquals(students.get(0).getName(), firstStudentData[0]);
+ assertEquals(String.join(";", students.get(0).getScore()), firstStudentData[1]);
+ }
+
+ @Test
+ void testWriteEmptyListDoesNothing() {
+ String fileName = "empty.csv";
+ assertThrows(IllegalArgumentException.class, () -> csvWriter.writeToFile(List.of(), fileName));
+ assertTrue(Files.notExists(Path.of(fileName)));
+ }
+
+ @Test
+ void testWriteToFileThatNotCsvSerializable() {
+
+ class UnannotatedClass {
+ private final String data;
+
+ public UnannotatedClass(String data) {
+ this.data = data;
+ }
+ }
+
+ List unannotatedList = List.of(new UnannotatedClass("some data"));
+ assertThrows(IllegalArgumentException.class, () -> csvWriter.writeToFile(unannotatedList, "should_fail.csv"));
+ }
+}