diff --git a/.gitignore b/.gitignore
index 5ff6309..1c6a0a7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,7 +3,11 @@ target/
!**/src/main/**/target/
!**/src/test/**/target/
+### files ##
+.csv
+
### IntelliJ IDEA ###
+.idea/*
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
diff --git a/src/main/java/org/writer/Main.java b/src/main/java/org/writer/Main.java
index 40e8213..cab3a80 100644
--- a/src/main/java/org/writer/Main.java
+++ b/src/main/java/org/writer/Main.java
@@ -1,7 +1,21 @@
package org.writer;
+import org.writer.csv.CsvDocument;
+import org.writer.model.Months;
+import org.writer.model.Person;
+
+import java.util.List;
+
public class Main {
public static void main(String[] args) {
- System.out.println("Hello world!");
+ var report = new Report(new CsvDocument());
+ report.writeObjectAsDoc(List.of(Person.builder()
+ .firstName("Mike")
+ .lastName("Petrov")
+ .dayOfBirth(10)
+ .monthOfBirth(Months.AUGUST)
+ .yearOfBirth(1988)
+ .password("myPassword")
+ .build()), "People");
}
}
\ No newline at end of file
diff --git a/src/main/java/org/writer/Report.java b/src/main/java/org/writer/Report.java
new file mode 100644
index 0000000..04bb9bf
--- /dev/null
+++ b/src/main/java/org/writer/Report.java
@@ -0,0 +1,41 @@
+package org.writer;
+
+import lombok.RequiredArgsConstructor;
+
+import java.util.List;
+
+/**
+ * Класс-обёртка над {@link Writable}, предоставляющий удобный метод
+ * для записи коллекции объектов в документ.
+ *
+ * Использует внедрённую стратегию {@link Writable}, что позволяет
+ * подменять реализацию (например, запись в CSV, JSON, XML и т.д.)
+ * без изменения кода клиента.
+ *
+ *
+ * Пример использования:
+ * {@code
+ * Writable csvWriter = new CsvDocument();
+ * Report report = new Report(csvWriter);
+ * report.writeObjectAsDoc(List.of(new Person("Alice", 30)), "people");
+ * }
+ */
+@RequiredArgsConstructor
+public class Report {
+
+ /**
+ * Стратегия записи, определяющая формат выходного документа.
+ */
+ private final Writable writable;
+
+ /**
+ * Записывает список объектов в документ, используя внедрённую стратегию {@link Writable}.
+ *
+ * @param data список объектов для сериализации
+ * @param fileName имя файла (без расширения, если реализация добавляет его сама)
+ * @throws RuntimeException если произошла ошибка при записи
+ */
+ public void writeObjectAsDoc(List> data, String fileName) {
+ writable.writeToFile(data, fileName);
+ }
+}
diff --git a/src/main/java/org/writer/csv/CsvDocument.java b/src/main/java/org/writer/csv/CsvDocument.java
new file mode 100644
index 0000000..dafefd5
--- /dev/null
+++ b/src/main/java/org/writer/csv/CsvDocument.java
@@ -0,0 +1,120 @@
+package org.writer.csv;
+
+import org.writer.Writable;
+import org.writer.csv.annotations.Transient;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+/**
+ * Реализация интерфейса {@link Writable}, позволяющая сериализовать список объектов в CSV-файл.
+ *
+ * Класс использует рефлексию для извлечения имён полей и их значений.
+ * Поля, помеченные аннотацией {@link Transient}, игнорируются при записи.
+ *
+ */
+public class CsvDocument implements Writable {
+
+ /** Расширение файлов по умолчанию. */
+ private static final String EXTENSION = ".csv";
+
+ /**
+ * Предикат для фильтрации полей, исключающий те, что помечены аннотацией {@link Transient}.
+ */
+ private final Predicate skipTransient = field -> !field.isAnnotationPresent(Transient.class);
+
+ /**
+ * Записывает список объектов в CSV-файл.
+ *
+ * @param data список объектов для сериализации
+ * @param fileName имя файла без расширения (расширение {@code .csv} добавляется автоматически)
+ * @throws RuntimeException если список пустой, равен {@code null} или произошла ошибка записи
+ */
+ @Override
+ public void writeToFile(List> data, String fileName) {
+ if (data == null || data.isEmpty())
+ throw new RuntimeException("Data list is null or empty!");
+
+ String form = form(data);
+ Path path = Path.of(fileName + EXTENSION);
+ try {
+ Files.writeString(path, form);
+ } catch (IOException e) {
+ throw new RuntimeException("Something went wrong while writing CSV file!", e);
+ }
+ }
+
+ /**
+ * Извлекает список имён полей объекта (колонки CSV).
+ * Поля с аннотацией {@link Transient} исключаются.
+ *
+ * @param instance объект, чьи поля анализируются
+ * @param тип объекта
+ * @return список имён полей
+ */
+ public List columns(T instance) {
+ Class> metadata = instance.getClass();
+ return Arrays.stream(metadata.getDeclaredFields())
+ .filter(skipTransient)
+ .map(Field::getName)
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Извлекает список значений полей объекта.
+ * Поля с аннотацией {@link Transient} исключаются.
+ *
+ * @param instance объект, чьи значения полей извлекаются
+ * @param тип объекта
+ * @return список строковых представлений значений
+ * @throws RuntimeException если доступ к полю невозможен
+ */
+ public List values(T instance) {
+ Class> metadata = instance.getClass();
+ List values = new ArrayList<>();
+ Field[] fields = Arrays.stream(metadata.getDeclaredFields())
+ .filter(skipTransient)
+ .toArray(Field[]::new);
+ for (Field field : fields) {
+ field.setAccessible(true);
+ try {
+ values.add(field.get(instance).toString());
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException("Access denied!", e);
+ }
+ }
+ return values;
+ }
+
+ /**
+ * Формирует строковое представление CSV-документа.
+ * Первая строка содержит заголовки (имена полей),
+ * последующие строки — значения объектов.
+ *
+ * @param data список объектов для сериализации
+ * @return строка в формате CSV
+ */
+ private String form(List> data) {
+ StringBuilder sb = new StringBuilder();
+
+ // Заголовки
+ columns(data.get(0)).forEach(i -> sb.append(i).append(", "));
+ sb.setLength(sb.length() - 2);
+ sb.append('\n');
+
+ // Значения
+ for (int i = 0; i < data.size(); i++) {
+ values(data.get(i)).forEach(v -> sb.append(v).append(", "));
+ sb.setLength(sb.length() - 2);
+ sb.append('\n');
+ }
+ return sb.toString();
+ }
+}
diff --git a/src/main/java/org/writer/csv/annotations/Transient.java b/src/main/java/org/writer/csv/annotations/Transient.java
new file mode 100644
index 0000000..dbdd7cf
--- /dev/null
+++ b/src/main/java/org/writer/csv/annotations/Transient.java
@@ -0,0 +1,36 @@
+package org.writer.csv.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Аннотация-маркер для исключения поля из сериализации в CSV-документ.
+ *
+ * Поля, помеченные этой аннотацией, будут проигнорированы при формировании
+ * заголовков и значений в {@code CsvDocument}.
+ *
+ *
+ * Особенности:
+ *
+ * - Применяется только к полям класса.
+ * - Сохраняется во время выполнения ({@link RetentionPolicy#RUNTIME}),
+ * что позволяет проверять её через рефлексию.
+ * - Не влияет на сам объект, только на процесс сериализации.
+ *
+ *
+ * Пример использования:
+ * {@code
+ * public class Person {
+ * private String name;
+ *
+ * @Transient
+ * private String password; // не попадёт в CSV
+ * }
+ * }
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.FIELD)
+public @interface Transient {
+}
diff --git a/src/main/java/org/writer/model/Person.java b/src/main/java/org/writer/model/Person.java
index ba8576b..c640ddb 100644
--- a/src/main/java/org/writer/model/Person.java
+++ b/src/main/java/org/writer/model/Person.java
@@ -3,6 +3,7 @@
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
+import org.writer.csv.annotations.Transient;
@Data
@Builder
@@ -18,5 +19,7 @@ public class Person {
private Months monthOfBirth;
private int yearOfBirth;
+ @Transient
+ private String password;
}
diff --git a/src/test/java/org/writer/csv/CsvDocumentTest.java b/src/test/java/org/writer/csv/CsvDocumentTest.java
new file mode 100644
index 0000000..f0486c0
--- /dev/null
+++ b/src/test/java/org/writer/csv/CsvDocumentTest.java
@@ -0,0 +1,83 @@
+package org.writer.csv;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.writer.csv.util.TestDataBuilder;
+import org.writer.model.Person;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class CsvDocumentTest {
+
+ private final CsvDocument csvDocument = new CsvDocument();
+ private final Path testFile = Path.of("test.csv");
+
+ @AfterEach
+ void cleanup() throws IOException {
+ Files.deleteIfExists(testFile);
+ }
+
+ @Test
+ void testColumnsExcludesTransient() {
+ Person p = TestDataBuilder.person();
+ List columns = csvDocument.columns(p);
+
+ assertTrue(columns.contains("firstName"));
+ assertTrue(columns.contains("lastName"));
+ assertTrue(columns.contains("dayOfBirth"));
+
+ // должно быть исключено
+ assertFalse(columns.contains("password"));
+ }
+
+ @Test
+ void testValuesExcludesTransient() {
+ Person p = TestDataBuilder.person();
+ List values = csvDocument.values(p);
+
+ assertTrue(values.contains(p.getFirstName()));
+ assertTrue(values.contains(p.getLastName()));
+ // пароль не должен попасть
+ assertFalse(values.contains(p.getPassword()));
+ }
+
+ @Test
+ void testWriteToFileCreatesCsv() throws IOException {
+ List people = TestDataBuilder.people();
+
+ csvDocument.writeToFile(people, "test");
+
+ assertTrue(Files.exists(testFile));
+
+ String content = Files.readString(testFile);
+ // Проверяем заголовки
+ assertTrue(content.startsWith("firstName, lastName, dayOfBirth"));
+ // Проверяем значения
+ Person p1 = people.get(0);
+ assertTrue(content.contains(String.format("%s, %s", p1.getFirstName(), p1.getLastName())));
+ Person p2 = people.get(1);
+ assertTrue(content.contains(String.format("%s, %s", p2.getFirstName(), p2.getLastName())));
+ // Убеждаемся, что паролей нет
+ assertFalse(content.contains("secret"));
+ assertFalse(content.contains("hidden"));
+ }
+
+ @Test
+ void testWriteToFileThrowsOnEmptyList() {
+ assertThrows(RuntimeException.class,
+ () -> csvDocument.writeToFile(List.of(), "empty"));
+ }
+
+ @Test
+ void testWriteToFileThrowsOnNull() {
+ assertThrows(RuntimeException.class,
+ () -> csvDocument.writeToFile(null, "null"));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/writer/csv/util/TestDataBuilder.java b/src/test/java/org/writer/csv/util/TestDataBuilder.java
new file mode 100644
index 0000000..6e13409
--- /dev/null
+++ b/src/test/java/org/writer/csv/util/TestDataBuilder.java
@@ -0,0 +1,26 @@
+package org.writer.csv.util;
+
+import org.writer.model.Months;
+import org.writer.model.Person;
+
+import java.util.List;
+
+public class TestDataBuilder {
+ public static Person person() {
+ return Person.builder()
+ .firstName("Ivan")
+ .lastName("Petrov")
+ .dayOfBirth(10)
+ .monthOfBirth(Months.AUGUST)
+ .yearOfBirth(1988)
+ .password("secret")
+ .build();
+ }
+
+ public static List people() {
+ return List.of(
+ Person.builder().firstName("Nina").lastName("Ivanova").dayOfBirth(10).monthOfBirth(Months.AUGUST).yearOfBirth(1994).password("secret").build(),
+ Person.builder().firstName("Bob").lastName("Smith").dayOfBirth(17).monthOfBirth(Months.JULY).yearOfBirth(1987).password("hidden").build()
+ );
+ }
+}