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 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/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 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 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"