Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@
<version>5.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.datafaker</groupId>
<artifactId>datafaker</artifactId>
<version>2.5.4</version>
<scope>compile</scope>
</dependency>
</dependencies>

</project>
20 changes: 19 additions & 1 deletion src/main/java/org/writer/Main.java
Original file line number Diff line number Diff line change
@@ -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<Student> students = DataGenerator.generateStudents(5);
writer.writeToFile(students, PATH + "students.csv");

List<Person> persons = DataGenerator.generatePersons(5);
writer.writeToFile(persons, PATH + "persons.csv");
}
}
27 changes: 27 additions & 0 deletions src/main/java/org/writer/annotation/ColumnCsv.java
Original file line number Diff line number Diff line change
@@ -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-файл
* <p>
* Позволяет задать пользовательское имя столбца в итоговом файле.
* Если имя не указано, рекомендуется использовать имя самого поля.
* </p>
*
* @author ZhelnovachevRoman
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface ColumnCsv {

/**
* Имя колонки в CSV-файле.
*
* @return имя колонки
*/
String name() default "";
}
7 changes: 7 additions & 0 deletions src/main/java/org/writer/exception/EmptyListException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.writer.exception;

public class EmptyListException extends RuntimeException {
public EmptyListException(String message) {
super(message);
}
}
7 changes: 7 additions & 0 deletions src/main/java/org/writer/exception/FileWritingException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.writer.exception;

public class FileWritingException extends RuntimeException {
public FileWritingException(String fileName) {
super("Ошибка записи файла " + fileName);
}
}
6 changes: 6 additions & 0 deletions src/main/java/org/writer/model/Person.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

}
3 changes: 3 additions & 0 deletions src/main/java/org/writer/model/Student.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import org.writer.annotation.ColumnCsv;

import java.util.List;

Expand All @@ -11,7 +12,9 @@
@AllArgsConstructor
public class Student {

@ColumnCsv(name = "имя")
private String name;

@ColumnCsv(name = "успеваемость")
private List<String> score;
}
93 changes: 93 additions & 0 deletions src/main/java/org/writer/service/CsvWriter.java
Original file line number Diff line number Diff line change
@@ -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}.
* <p>
* Класс предназначен для сохранения коллекций объектов в виде таблицы CSV.
* Если значение поля является {@link List}, элементы списка объединяются через «;».
* </p>
*
* @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(","));
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.writer;
package org.writer.service;

import java.util.List;

Expand Down
73 changes: 73 additions & 0 deletions src/main/java/org/writer/util/DataGenerator.java
Original file line number Diff line number Diff line change
@@ -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}.
* <p>
* Предоставляет методы для создания коллекций объектов {@link Student} и {@link Person}
* с заполненными случайными значениями полями. Используется для тестирования
* и демонстрации функциональности.
* </p>
*
* @author ZhelnovachevRoman
*/
public class DataGenerator {

private static final Faker faker = new Faker();

/**
* Генерирует список студентов с случайными именами и оценками.
* <p>
* Каждый студент получает случайное имя и список из трёх оценок
* в диапазоне от 1 до 5.
* </p>
*
* @param count количество студентов для генерации.
* @return список объектов {@link Student} с заполненными полями.
*/
public static List<Student> generateStudents(int count){
List<Student> 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;
}

/**
* Генерирует список персон с случайными ФИО и датами рождения.
* <p>
* Каждая персона получает случайное имя, фамилию, день рождения (1-28),
* месяц рождения (из перечисления {@link Months}) и год рождения (1900-2025).
* </p>
*
* @param count количество персон для генерации.
* @return список объектов {@link Person} с заполненными полями.
*/
public static List<Person> generatePersons(int count){
List<Person> 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;
}
}
59 changes: 59 additions & 0 deletions src/test/java/org/writer/service/CsvWriterTest.java
Original file line number Diff line number Diff line change
@@ -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<Student> 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<Student> emptyList = List.of();
EmptyListException exception = assertThrows(
EmptyListException.class,
() -> csvWriter.writeToFile(emptyList, "test.csv")
);
assertEquals("Список пуст", exception.getMessage());
}
}
Loading