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
17 changes: 15 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,23 @@
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.8.1</version>
<artifactId>junit-jupiter-api</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>net.datafaker</groupId>
<artifactId>datafaker</artifactId>
<version>2.0.2</version>
</dependency>
</dependencies>

</project>
133 changes: 133 additions & 0 deletions src/main/java/org/writer/CsvWriter.java
Original file line number Diff line number Diff line change
@@ -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 файл.
* <p>
* Данная реализация использует рефлексию, а также кастомные аннотации ({@link CsvSerializable},
* {@link CsvColumn}) для валидации данных, подлежащих записи.
*/
public class CsvWriter implements Writable {

/**
* Разделитель, используемый для отделения значений в строке CSV.
*/
private final static String SEPARATOR = ",";

/**
* Разделитель, используемый для объединения элементов списка в одну ячейку CSV.
*/
private static final String LIST_ITEM_DELIM = ";";

/**
* Записывает список объектов в CSV файл.
* <p>
* Сначала записывается строка заголовка, основанная на именах колонок,
* а затем для каждого объекта из списка записывается строка данных.
*
* @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<Field> 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<Field> getCsvFields(Class<?> clazz){
return Stream.of(clazz.getDeclaredFields())
.filter(field -> field.isAnnotationPresent(CsvColumn.class))
.collect(Collectors.toList());
}

/**
* Определяет имя колонки для заданного поля.
* <p>
* В приоритете используется атрибут {@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;
}

/**
* Извлекает строковое представление значения поля из заданного объекта.
* <p>
* Предоставляет специальную обработку для полей типа {@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);
}
}
}
42 changes: 41 additions & 1 deletion src/main/java/org/writer/Main.java
Original file line number Diff line number Diff line change
@@ -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<Person> 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<Student> 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");
}
}
19 changes: 19 additions & 0 deletions src/main/java/org/writer/annotation/CsvColumn.java
Original file line number Diff line number Diff line change
@@ -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 "";
}
15 changes: 15 additions & 0 deletions src/main/java/org/writer/annotation/CsvSerializable.java
Original file line number Diff line number Diff line change
@@ -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 формат.
* <p>CsvWriter будет обрабатывать только классы, помеченные этой аннотацией.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface CsvSerializable {
}
9 changes: 8 additions & 1 deletion src/main/java/org/writer/model/Person.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

}
5 changes: 5 additions & 0 deletions src/main/java/org/writer/model/Student.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> score;
}
Loading