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
15 changes: 15 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,27 @@
<version>1.18.34</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.13</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.datafaker</groupId>
<artifactId>datafaker</artifactId>
<version>2.5.3</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.5.20</version>
</dependency>
</dependencies>

</project>
137 changes: 137 additions & 0 deletions src/main/java/org/writer/CsvWriter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package org.writer;

import lombok.extern.slf4j.Slf4j;
import org.writer.annotation.CsvColumn;
import org.writer.exception.CsvWriteException;

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
* Класс для записи объектов в CSV формат.
* <p>
* Класс использует рефлексию: поля, помеченные аннотацией {@link CsvColumn},
* извлекаются, сортируются по порядку и записываются в файл как строки CSV.
* Поддерживается экранирование значений, содержащих запятые, кавычки или переносы строки.
* </p>
*/
@Slf4j
public class CsvWriter implements Writable {

/**
* Записывает список объектов в CSV-файл.
*
* @param data список объектов, которые необходимо сохранить
* @param fileName имя создаваемого файла (существующий будет перезаписан)
* @throws IllegalArgumentException если список пустой или null
* @throws CsvWriteException при ошибке ввода-вывода
*/
@Override
public void writeToFile(List<?> data, String fileName) {
if (data == null || data.isEmpty()) {
log.error("Data list is empty");
throw new IllegalArgumentException("Data list is empty");
}

Class<?> clazz = data.get(0).getClass();
List<Field> csvFields = getCsvFields(clazz);

try (BufferedWriter writer = new BufferedWriter(new FileWriter(fileName))) {
writer.write(buildHeader(csvFields));
writer.newLine();

for (Object object : data) {
writer.write(buildRow(object, csvFields));
writer.newLine();
}

} catch (IOException e) {
log.error(e.getMessage());
throw new CsvWriteException("Failed to write CSV file: " + fileName);
}
}

/**
* Возвращает отсортированный список полей с аннотацией {@link CsvColumn}.
*
* @param clazz класс модели
* @return список отсортированных полей
*/
private List<Field> getCsvFields(Class<?> clazz) {
return Stream.of(clazz.getDeclaredFields())
.filter(field -> field.isAnnotationPresent(CsvColumn.class))
.sorted(Comparator.comparingInt(field -> field.getAnnotation(CsvColumn.class).order()))
.collect(Collectors.toList());
}

/**
* Формирует строку заголовка CSV на основе имени столбца из аннотации {@link CsvColumn}.
*
* @param fields список полей
* @return строка заголовка
*/
private String buildHeader(List<Field> fields) {
return fields.stream()
.map(field -> field.getAnnotation(CsvColumn.class).name())
.collect(Collectors.joining(","));
}

/**
* Формирует строку CSV, извлекая значения полей объекта.
*
* @param object объект модели
* @param fields список полей для экспорта
* @return строка в формате CSV
*/
private String buildRow(Object object, List<Field> fields) {
return fields.stream()
.map(field -> {
try {
field.setAccessible(true);
return escapeCsvValue(convertValue(field.get(object)));
} catch (IllegalAccessException e) {
log.error(e.getMessage());
throw new CsvWriteException(e.getMessage());
}
})
.collect(Collectors.joining(","));
}

/**
* Преобразует значение поля в строку.
*
* @param value значение поля
* @return строковое представление
*/
private String convertValue(Object value) {
if (value == null) {
return "";
}

if (value instanceof List<?> list) {
return list.toString();
}

return value.toString();
}

/**
* Экранирует значение для корректной записи в CSV.
*
* @param value исходное значение
* @return экранированное значение
*/
private String escapeCsvValue(String value) {
if (value.contains(",") || value.contains("\"") || value.contains("\n")) {
value = value.replace("\"", "\"\"");
return "\"" + value + "\"";
}
return value.trim();
}
}
50 changes: 49 additions & 1 deletion src/main/java/org/writer/Main.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,55 @@
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.ArrayList;
import java.util.List;
import java.util.Random;

public class Main {

private static final String PERSON_FILE_NAME = "persons.csv";
private static final String STUDENT_FILE_NAME = "students.csv";

public static void main(String[] args) {
System.out.println("Hello world!");
Writable csvWriter = new CsvWriter();
Faker faker = new Faker();
Random random = new Random();

List<Person> people = new ArrayList<>();
for (int i = 0; i < 10; i++) {
people.add(
Person.builder()
.firstName(faker.name().firstName())
.lastName(faker.name().lastName())
.dayOfBirth(faker.number().numberBetween(1, 28))
.monthOfBirth(Months.values()[random.nextInt(Months.values().length)])
.yearOfBirth(faker.number().numberBetween(1941, 2025))
.build()
);
}

csvWriter.writeToFile(people, PERSON_FILE_NAME);

List<Student> students = new ArrayList<>();
for (int i = 0; i < 10; i++) {
List<String> scores = List.of(
String.valueOf(faker.number().numberBetween(1, 5)),
String.valueOf(faker.number().numberBetween(1, 5)),
String.valueOf(faker.number().numberBetween(1, 5))
);

students.add(
Student.builder()
.name(faker.name().fullName())
.score(scores)
.build()
);
}

csvWriter.writeToFile(students, STUDENT_FILE_NAME);
}
}
24 changes: 24 additions & 0 deletions src/main/java/org/writer/annotation/CsvColumn.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
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.
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CsvColumn {

/**
* Имя столбца в CSV.
*/
String name() default "";

/**
* Порядок следования столбца в CSV.
*/
int order() default 0;
}
11 changes: 11 additions & 0 deletions src/main/java/org/writer/exception/CsvWriteException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.writer.exception;

/**
* Исключение, которое выбрасывается при ошибках записи CSV-файла.
*/
public class CsvWriteException extends RuntimeException {

public CsvWriteException(String message) {
super(message);
}
}
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.CsvColumn;

@Data
@Builder
@AllArgsConstructor
public class Person {

@CsvColumn(name = "first_name", order = 1)
private String firstName;

@CsvColumn(name = "last_name", order = 2)
private String lastName;

@CsvColumn(name = "day_of_birth", order = 3)
private int dayOfBirth;

@CsvColumn(name = "month_of_birth", order = 4)
private Months monthOfBirth;

@CsvColumn(name = "year_of_birth", order = 5)
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.CsvColumn;

import java.util.List;

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

@CsvColumn(name = "name", order = 1)
private String name;

@CsvColumn(name = "score", order = 2)
private List<String> score;
}
Loading