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
8 changes: 8 additions & 0 deletions .idea/.gitignore

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

16 changes: 16 additions & 0 deletions .idea/checkstyle-idea.xml

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

13 changes: 13 additions & 0 deletions .idea/misc.xml

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

6 changes: 6 additions & 0 deletions .idea/vcs.xml

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

11 changes: 11 additions & 0 deletions people.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
first_name,last_name,day_of_birth,month_of_birth,year_of_birth
Юлия,Авдеев,22,JULY,1978
София,Киселев,27,MAY,2005
Валентин,Поляков,10,MAY,1983
Аркадий,Куликов,10,OCTOBER,1981
Валерий,Матвеев,15,AUGUST,1992
Сергей,Баранова,19,APRIL,1979
Надежда,Зуев,5,JANUARY,1984
Матвей,Крылова,22,SEPTEMBER,1980
Олег,Лобанова,17,FEBRUARY,1999
Анжелика,Ширяева,11,AUGUST,1972
11 changes: 11 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@
<version>5.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.javafaker</groupId>
<artifactId>javafaker</artifactId>
<version>1.0.2</version>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.24.2</version>
<scope>test</scope>
</dependency>
</dependencies>

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

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Аннотация для пометки полей, которые должны попадать в CSV. * Поле order используется для задания порядка колонок в выходном файле.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface CsvColumn {

String name() default "";

int order() default Integer.MAX_VALUE;

}
117 changes: 117 additions & 0 deletions src/main/java/org/writer/CsvWritable.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package org.writer;

import org.apache.commons.lang3.StringUtils;

import java.io.BufferedWriter;
import java.io.IOException;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;

import static java.nio.file.StandardOpenOption.CREATE;
import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
import static java.util.stream.Collectors.joining;

/**
* Реализация Writable, сохраняющая список объектов в CSV файл
*/
public class CsvWritable implements Writable {

private final String separator;

/**
* Создаёт CsvWritable с разделителем по умолчанию (запятая).
*/
public CsvWritable() {
this(",");
}

/**
* Создаёт CsvWritable с указанным разделителем.
*
* @param separator строка-разделитель
*/
public CsvWritable(String separator) {
if (StringUtils.isEmpty(separator)) {
throw new IllegalArgumentException("Сепаратор не заполнен");
}
this.separator = separator;
}

/**
* Сохраняет список объектов в CSV файл
*
* @param data список объектов одного типа
* @param fileName путь к файлу (создаётся/перезаписывается)
*/
@Override
public void writeToFile(List<?> data, String fileName) {
if (data == null || data.isEmpty()) {
throw new IllegalArgumentException("Данные не заполнены");
}
Class<?> clazz = data.get(0).getClass();
List<Field> columns = getAnnotatedFields(clazz);
List<String> header = columns.stream().map(f -> {
CsvColumn ann = f.getAnnotation(CsvColumn.class);
return ann.name().isEmpty()
? f.getName()
: ann.name();
}).toList();
Path path = Path.of(fileName);
try (BufferedWriter writer = Files.newBufferedWriter(path, CREATE, TRUNCATE_EXISTING)) {
writer.write(String.join(separator, header));
writer.newLine();
for (Object obj : data) {
List<String> row = new ArrayList<>();
for (Field field : columns) {
field.setAccessible(true);
Object value = field.get(obj);
row.add(escapeCsv(formatValue(value)));
}
writer.write(String.join(separator, row));
writer.newLine();
}
} catch (IOException | IllegalAccessException e) {
throw new RuntimeException("Ошибка записи CSV файла", e);
}
}

private List<Field> getAnnotatedFields(Class<?> clazz) {
Field[] fields = clazz.getDeclaredFields();
return Arrays.stream(fields)
.filter(f -> f.isAnnotationPresent(CsvColumn.class))
.sorted(Comparator.comparingInt(f -> f.getAnnotation(CsvColumn.class).order()))
.toList();
}

private String formatValue(Object value) {
if (value == null) {
return "";
}
if (value instanceof Enum<?>) {
return ((Enum<?>) value).name();
}
if (value instanceof Collection<?>) {
return ((Collection<?>) value).stream()
.map(Objects::toString)
.collect(joining(";"));
}
return value.toString();
}

private String escapeCsv(String field) {
if (StringUtils.isEmpty(field)) {
return "";
}
boolean needQuotes = field.contains(",")
|| field.contains("\"")
|| field.contains("\n")
|| field.contains("\r")
|| field.contains(";");
String escaped = field.replace("\"", "\"\"");
return needQuotes
? "\"" + escaped + "\""
: escaped;
}
}
35 changes: 34 additions & 1 deletion src/main/java/org/writer/Main.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,40 @@
package org.writer;

import com.github.javafaker.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.Locale;
import java.util.stream.IntStream;

public class Main {
public static void main(String[] args) {
System.out.println("Hello world!");
Faker faker = new Faker(new Locale("ru"));
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, Months.values().length)])
.yearOfBirth(faker.number().numberBetween(1970, 2010))
.build())
.toList();
List<Student> students = new ArrayList<>();
for (int i = 0; i < 5; i++) {
List<String> scores = IntStream.range(0, 4)
.mapToObj(j -> String.valueOf(faker.number().numberBetween(50, 100)))
.toList();
students.add(Student.builder()
.name(faker.name().fullName())
.score(scores)
.build());
}
Writable writer = new CsvWritable();
writer.writeToFile(people, "people.csv");
writer.writeToFile(students, "students.csv");
System.out.println("CSV файлы (people.csv, students.csv) успешно заполнены");
}
}
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.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.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;
}
68 changes: 68 additions & 0 deletions src/test/java/org/writer/CsvWritableTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package org.writer;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.writer.model.Months;
import org.writer.model.Person;
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.assertj.core.api.Assertions.assertThat;

class CsvWritableTest {

private Writable writable;

@TempDir
Path tempDir;

@BeforeEach
void setUp() {
writable = new CsvWritable();
}

@Test
void writesPersonCsv() throws IOException {
Person p1 = Person.builder()
.firstName("Ivan")
.lastName("Petrov")
.dayOfBirth(5)
.monthOfBirth(Months.MARCH)
.yearOfBirth(1990)
.build();
Person p2 = Person.builder()
.firstName("Anna")
.lastName("Ivanova")
.dayOfBirth(12)
.monthOfBirth(Months.DECEMBER)
.yearOfBirth(1985)
.build();
Path file = tempDir.resolve("persons.csv");

writable.writeToFile(List.of(p1, p2), file.toString());

String content = Files.readString(file);
assertThat(content).contains("first_name,last_name,day_of_birth,month_of_birth,year_of_birth");
assertThat(content).contains("Ivan,Petrov,5,MARCH,1990");
assertThat(content).contains("Anna,Ivanova,12,DECEMBER,1985");
}

@Test
void writesStudentCsvWithListField() throws IOException {
Student s = Student.builder().name("Sidor Sidorov").score(List.of("80", "90", "75")).build();
Path file = tempDir.resolve("students.csv");

writable.writeToFile(List.of(s), file.toString());

String content = Files.readString(file);
assertThat(content).contains("name,score");
assertThat(content).contains("Sidor Sidorov");
assertThat(content).contains("80;90;75");
}

}
6 changes: 6 additions & 0 deletions students.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
name,score
Илья Алексеев,"97;54;82;61"
Валентин Анисимов,"60;67;53;55"
Ирина Мельникова,"87;57;88;67"
Анна Леонидовна Романова,"64;64;63;63"
Мишина Клавдия,"74;81;98;89"