From dc2204ce2c6ab58c4d32e8a688cb7d2b311bf488 Mon Sep 17 00:00:00 2001 From: PAVEL FILONOV Date: Mon, 2 Feb 2026 23:25:04 +0300 Subject: [PATCH 1/4] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D1=81=D0=BB=D1=83=D0=B6=D0=B5=D0=B1=D0=BD=D1=8B?= =?UTF-8?q?=D0=B5=20=D1=84=D0=B0=D0=B9=D0=BB=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/.gitignore | 8 ++++++++ .idea/checkstyle-idea.xml | 16 ++++++++++++++++ .idea/misc.xml | 13 +++++++++++++ .idea/vcs.xml | 6 ++++++ 4 files changed, 43 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/checkstyle-idea.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/vcs.xml diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/checkstyle-idea.xml b/.idea/checkstyle-idea.xml new file mode 100644 index 0000000..95a8d96 --- /dev/null +++ b/.idea/checkstyle-idea.xml @@ -0,0 +1,16 @@ + + + + 10.26.1 + JavaOnly + true + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..52d3977 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,13 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file From f643539d7bfb5ce9db2efbab9d444f10816aebc5 Mon Sep 17 00:00:00 2001 From: PAVEL FILONOV Date: Tue, 3 Feb 2026 00:25:07 +0300 Subject: [PATCH 2/4] =?UTF-8?q?=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D0=BD=D0=BE=20=D1=81=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20CSV=20=D1=84=D0=B0=D0=B9=D0=BB=D0=BE=D0=B2=20?= =?UTF-8?q?=D1=81=20=D0=B7=D0=B0=D0=BF=D0=BE=D0=BB=D0=BD=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=D0=BC=20=D0=BF=D0=BE=20=D0=BE=D0=B1=D1=8A=D0=B5=D0=BA?= =?UTF-8?q?=D1=82=D0=B0=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/org/writer/CsvColumn.java | 19 ++++ src/main/java/org/writer/CsvWritable.java | 117 ++++++++++++++++++++ src/main/java/org/writer/Main.java | 35 +++++- src/main/java/org/writer/model/Person.java | 6 + src/main/java/org/writer/model/Student.java | 3 + 5 files changed, 179 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/writer/CsvColumn.java create mode 100644 src/main/java/org/writer/CsvWritable.java diff --git a/src/main/java/org/writer/CsvColumn.java b/src/main/java/org/writer/CsvColumn.java new file mode 100644 index 0000000..f28540f --- /dev/null +++ b/src/main/java/org/writer/CsvColumn.java @@ -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; + +} diff --git a/src/main/java/org/writer/CsvWritable.java b/src/main/java/org/writer/CsvWritable.java new file mode 100644 index 0000000..c6d1637 --- /dev/null +++ b/src/main/java/org/writer/CsvWritable.java @@ -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 columns = getAnnotatedFields(clazz); + List 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 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 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; + } +} diff --git a/src/main/java/org/writer/Main.java b/src/main/java/org/writer/Main.java index 40e8213..718943c 100644 --- a/src/main/java/org/writer/Main.java +++ b/src/main/java/org/writer/Main.java @@ -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 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 students = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + List 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) успешно заполнены"); } } \ No newline at end of file diff --git a/src/main/java/org/writer/model/Person.java b/src/main/java/org/writer/model/Person.java index ba8576b..82ca71a 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.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; } diff --git a/src/main/java/org/writer/model/Student.java b/src/main/java/org/writer/model/Student.java index 2b549c4..6011853 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.CsvColumn; import java.util.List; @@ -11,7 +12,9 @@ @AllArgsConstructor public class Student { + @CsvColumn(name = "name", order = 1) private String name; + @CsvColumn(name = "score", order = 2) private List score; } \ No newline at end of file From ed1fcff1523e23ab789c477c7681305b5dadaf60 Mon Sep 17 00:00:00 2001 From: PAVEL FILONOV Date: Tue, 3 Feb 2026 00:26:04 +0300 Subject: [PATCH 3/4] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D1=82=D0=B5=D1=81=D1=82=D1=8B=20=D0=B8=20=D0=BF?= =?UTF-8?q?=D1=80=D0=B8=D0=BC=D0=B5=D1=80=D1=8B=20=D1=81=D1=84=D0=BE=D1=80?= =?UTF-8?q?=D0=BC=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20?= =?UTF-8?q?CSV=20=D1=84=D0=B0=D0=B9=D0=BB=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- people.csv | 11 +++ src/test/java/org/writer/CsvWritableTest.java | 68 +++++++++++++++++++ students.csv | 6 ++ 3 files changed, 85 insertions(+) create mode 100644 people.csv create mode 100644 src/test/java/org/writer/CsvWritableTest.java create mode 100644 students.csv diff --git a/people.csv b/people.csv new file mode 100644 index 0000000..945fda0 --- /dev/null +++ b/people.csv @@ -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 diff --git a/src/test/java/org/writer/CsvWritableTest.java b/src/test/java/org/writer/CsvWritableTest.java new file mode 100644 index 0000000..c431787 --- /dev/null +++ b/src/test/java/org/writer/CsvWritableTest.java @@ -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"); + } + +} \ No newline at end of file diff --git a/students.csv b/students.csv new file mode 100644 index 0000000..a7b3ff0 --- /dev/null +++ b/students.csv @@ -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" From 349313e08a5d59b854f52dc0ddb02d34b517c56b Mon Sep 17 00:00:00 2001 From: PAVEL FILONOV Date: Tue, 3 Feb 2026 00:27:01 +0300 Subject: [PATCH 4/4] =?UTF-8?q?=D0=94=D0=BE=D0=BF=D0=BE=D0=BB=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=B7=D0=B0=D0=B2=D0=B8=D1=81=D0=B8=D0=BC=D0=BE?= =?UTF-8?q?=D1=81=D1=82=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pom.xml b/pom.xml index effc1bd..05c5d95 100644 --- a/pom.xml +++ b/pom.xml @@ -27,6 +27,17 @@ 5.8.1 test + + com.github.javafaker + javafaker + 1.0.2 + + + org.assertj + assertj-core + 3.24.2 + test + \ No newline at end of file