diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/pom.xml b/pom.xml index effc1bd..6001b15 100644 --- a/pom.xml +++ b/pom.xml @@ -27,6 +27,12 @@ 5.8.1 test + + net.datafaker + datafaker + 2.5.4 + compile + \ 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..225b2ed 100644 --- a/src/main/java/org/writer/Main.java +++ b/src/main/java/org/writer/Main.java @@ -1,7 +1,25 @@ package org.writer; +import org.writer.model.Person; +import org.writer.model.Student; +import org.writer.service.CsvWriter; +import org.writer.service.Writable; +import org.writer.util.DataGenerator; + +import java.util.List; + public class Main { + + private static final String PATH = "reports/"; + public static void main(String[] args) { - System.out.println("Hello world!"); + + Writable writer = new CsvWriter(); + + List students = DataGenerator.generateStudents(5); + writer.writeToFile(students, PATH + "students.csv"); + + List persons = DataGenerator.generatePersons(5); + writer.writeToFile(persons, PATH + "persons.csv"); } } \ No newline at end of file diff --git a/src/main/java/org/writer/annotation/ColumnCsv.java b/src/main/java/org/writer/annotation/ColumnCsv.java new file mode 100644 index 0000000..23b24f1 --- /dev/null +++ b/src/main/java/org/writer/annotation/ColumnCsv.java @@ -0,0 +1,27 @@ +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-файл + *

+ * Позволяет задать пользовательское имя столбца в итоговом файле. + * Если имя не указано, рекомендуется использовать имя самого поля. + *

+ * + * @author ZhelnovachevRoman + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface ColumnCsv { + + /** + * Имя колонки в CSV-файле. + * + * @return имя колонки + */ + String name() default ""; +} diff --git a/src/main/java/org/writer/exception/EmptyListException.java b/src/main/java/org/writer/exception/EmptyListException.java new file mode 100644 index 0000000..2012468 --- /dev/null +++ b/src/main/java/org/writer/exception/EmptyListException.java @@ -0,0 +1,7 @@ +package org.writer.exception; + +public class EmptyListException extends RuntimeException { + public EmptyListException(String message) { + super(message); + } +} diff --git a/src/main/java/org/writer/exception/FileWritingException.java b/src/main/java/org/writer/exception/FileWritingException.java new file mode 100644 index 0000000..c53b1f1 --- /dev/null +++ b/src/main/java/org/writer/exception/FileWritingException.java @@ -0,0 +1,7 @@ +package org.writer.exception; + +public class FileWritingException extends RuntimeException { + public FileWritingException(String fileName) { + super("Ошибка записи файла " + fileName); + } +} diff --git a/src/main/java/org/writer/model/Person.java b/src/main/java/org/writer/model/Person.java index ba8576b..b2b1ab9 100644 --- a/src/main/java/org/writer/model/Person.java +++ b/src/main/java/org/writer/model/Person.java @@ -3,20 +3,26 @@ import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import org.writer.annotation.ColumnCsv; @Data @Builder @AllArgsConstructor public class Person { + @ColumnCsv(name = "имя") private String firstName; + @ColumnCsv(name = "фамилия") private String lastName; + @ColumnCsv(name = "день_рождения") private int dayOfBirth; + @ColumnCsv(name = "месяц_рождения") private Months monthOfBirth; + @ColumnCsv(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..6a105f4 100644 --- a/src/main/java/org/writer/model/Student.java +++ b/src/main/java/org/writer/model/Student.java @@ -3,6 +3,7 @@ import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import org.writer.annotation.ColumnCsv; import java.util.List; @@ -11,7 +12,9 @@ @AllArgsConstructor public class Student { + @ColumnCsv(name = "имя") private String name; + @ColumnCsv(name = "успеваемость") private List 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..7462219 --- /dev/null +++ b/src/main/java/org/writer/service/CsvWriter.java @@ -0,0 +1,93 @@ +package org.writer.service; + +import org.writer.annotation.ColumnCsv; +import org.writer.exception.EmptyListException; +import org.writer.exception.FileWritingException; + +import java.io.BufferedWriter; +import java.io.FileWriter; +import java.io.IOException; +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Реализация интерфейса {@link Writable}, выполняющая сериализацию + * списка объектов в CSV-файл с использованием Reflection и аннотации {@link ColumnCsv}. + *

+ * Класс предназначен для сохранения коллекций объектов в виде таблицы CSV. + * Если значение поля является {@link List}, элементы списка объединяются через «;». + *

+ * + * @author ZhelnovachevRoman + */ +public class CsvWriter implements Writable { + + @Override + public void writeToFile(List data, + String fileName) { + if (data == null || data.isEmpty()) { + throw new EmptyListException("Список пуст"); + } + Class ourClass = data.get(0) + .getClass(); + Field[] fields = ourClass.getDeclaredFields(); + try (BufferedWriter writer = new BufferedWriter(new FileWriter(fileName))) { + String header = headerConstructor(fields); + writer.write(header); + writer.newLine(); + for (Object object : data) { + String line = lineConstructor(object, + fields); + writer.write(line); + writer.newLine(); + } + } catch (IOException e){ + throw new FileWritingException(fileName); + } + } + + /** + * Формирует строку заголовков CSV на основе аннотаций {@link ColumnCsv}. + * + * @param fields массив полей класса, извлечённых через Reflection. + * @return строка заголовков, объединённая через запятую. + */ + private String headerConstructor(Field[] fields){ + return Arrays.stream(fields) + .map(field -> { + ColumnCsv annotation = field.getAnnotation(ColumnCsv.class); + return annotation.name(); + }) + .collect(Collectors.joining(",")); + } + + /** + * Формирует строку данных для одного объекта. + * Если поле содержит список, его элементы соединяются через «;». + * + * @param object объект, из которого извлекаются значения. + * @param fields массив полей класса, доступных для чтения. + * @return строка данных, объединённая через запятую. + */ + private String lineConstructor(Object object, Field[] fields) { + return Arrays.stream(fields) + .map(field -> { + field.setAccessible(true); + Object value; + try { + value = field.get(object); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + if (value instanceof List list) { + return list.stream() + .map(Object::toString) + .collect(Collectors.joining(";")); + } + return String.valueOf(value); + }) + .collect(Collectors.joining(",")); + } +} diff --git a/src/main/java/org/writer/Writable.java b/src/main/java/org/writer/service/Writable.java similarity index 79% rename from src/main/java/org/writer/Writable.java rename to src/main/java/org/writer/service/Writable.java index 9f60c45..fe1afa9 100644 --- a/src/main/java/org/writer/Writable.java +++ b/src/main/java/org/writer/service/Writable.java @@ -1,4 +1,4 @@ -package org.writer; +package org.writer.service; import java.util.List; diff --git a/src/main/java/org/writer/util/DataGenerator.java b/src/main/java/org/writer/util/DataGenerator.java new file mode 100644 index 0000000..c919e5c --- /dev/null +++ b/src/main/java/org/writer/util/DataGenerator.java @@ -0,0 +1,73 @@ +package org.writer.util; + +import net.datafaker.Faker; +import org.writer.model.Months; +import org.writer.model.Person; +import org.writer.model.Student; + +import java.util.ArrayList; +import java.util.List; + +/** + * Утилитный класс для генерации тестовых данных с использованием библиотеки {@link Faker}. + *

+ * Предоставляет методы для создания коллекций объектов {@link Student} и {@link Person} + * с заполненными случайными значениями полями. Используется для тестирования + * и демонстрации функциональности. + *

+ * + * @author ZhelnovachevRoman + */ +public class DataGenerator { + + private static final Faker faker = new Faker(); + + /** + * Генерирует список студентов с случайными именами и оценками. + *

+ * Каждый студент получает случайное имя и список из трёх оценок + * в диапазоне от 1 до 5. + *

+ * + * @param count количество студентов для генерации. + * @return список объектов {@link Student} с заполненными полями. + */ + public static List generateStudents(int count){ + List students = new ArrayList<>(); + for (int i = 0; i < count; i++) { + students.add(Student.builder() + .name(faker.name().firstName()) + .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()); + } + return students; + } + + /** + * Генерирует список персон с случайными ФИО и датами рождения. + *

+ * Каждая персона получает случайное имя, фамилию, день рождения (1-28), + * месяц рождения (из перечисления {@link Months}) и год рождения (1900-2025). + *

+ * + * @param count количество персон для генерации. + * @return список объектов {@link Person} с заполненными полями. + */ + public static List generatePersons(int count){ + List persons = new ArrayList<>(); + for (int i = 0; i < count; i++) { + persons.add(Person.builder() + .firstName(faker.name().firstName()) + .lastName(faker.name().lastName()) + .dayOfBirth(faker.number().numberBetween(1, 28)) + .monthOfBirth(faker.options().option(Months.class)) + .yearOfBirth(faker.number().numberBetween(1900, 2025)) + .build()); + } + return persons; + } +} 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..9769547 --- /dev/null +++ b/src/test/java/org/writer/service/CsvWriterTest.java @@ -0,0 +1,59 @@ +package org.writer.service; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.writer.exception.EmptyListException; +import org.writer.model.Student; + +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.*; + +public class CsvWriterTest { + private CsvWriter csvWriter; + + @TempDir + Path tempDir; + + @BeforeEach + void setUp() { + csvWriter = new CsvWriter(); + } + + @Test + @DisplayName("Корректная запись в CSV-файл") + void writeToFile_shouldWriteCorrectCsvFormat() throws IOException { + List students = List.of( + Student.builder() + .name("Иван") + .score(List.of("5", "4", "5")) + .build(), + Student.builder() + .name("Мария") + .score(List.of("3", "4", "4")) + .build() + ); + Path filePath = tempDir.resolve("test.csv"); + csvWriter.writeToFile(students, filePath.toString()); + String content = Files.readString(filePath); + assertTrue(content.contains("имя,успеваемость"), "Должен содержать заголовок"); + assertTrue(content.contains("Иван,5;4;5"), "Должен содержать первую строку"); + assertTrue(content.contains("Мария,3;4;4"), "Должен содержать вторую строку"); + } + + @Test + @DisplayName("Пустой список студентов - выбрасывает кастомное исключение") + void writeToFile_withEmptyList_shouldThrowException() { + List emptyList = List.of(); + EmptyListException exception = assertThrows( + EmptyListException.class, + () -> csvWriter.writeToFile(emptyList, "test.csv") + ); + assertEquals("Список пуст", exception.getMessage()); + } +} diff --git a/src/test/java/org/writer/util/DataGeneratorTest.java b/src/test/java/org/writer/util/DataGeneratorTest.java new file mode 100644 index 0000000..927166d --- /dev/null +++ b/src/test/java/org/writer/util/DataGeneratorTest.java @@ -0,0 +1,40 @@ +package org.writer.util; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.writer.model.Person; +import org.writer.model.Student; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +public class DataGeneratorTest { + + @Test + @DisplayName("Генерация студентов: должен вернуть список указанного размера") + void generateStudents_shouldReturnListOfSpecifiedSize() { + int count = 5; + List students = DataGenerator.generateStudents(count); + assertNotNull(students, "Список не должен быть null"); + assertEquals(count, students.size(), "Размер списка должен соответствовать запрошенному"); + } + + @Test + @DisplayName("Генерация персон: должен вернуть список с валидными данными") + void generatePersons_shouldReturnListOfValidData() { + int count = 3; + List persons = DataGenerator.generatePersons(count); + assertNotNull(persons, "Список не должен быть null"); + assertEquals(count, persons.size(), "Размер списка должен соответствовать запрошенному"); + for (Person person : persons) { + assertNotNull(person.getFirstName(), "Имя не должно быть null"); + assertNotNull(person.getLastName(), "Фамилия не должна быть null"); + assertNotNull(person.getMonthOfBirth(), "Месяц рождения не должен быть null"); + assertTrue(person.getDayOfBirth() >= 1 && person.getDayOfBirth() <= 28, + "День рождения должен быть в диапазоне 1-28"); + assertTrue(person.getYearOfBirth() >= 1900 && person.getYearOfBirth() <= 2025, + "Год рождения должен быть в диапазоне 1900-2025"); + } + } +}