From 29e86888f4fbaca487299658050088f90620725b Mon Sep 17 00:00:00 2001 From: Mikhail Date: Wed, 15 Oct 2025 10:06:58 +0300 Subject: [PATCH 1/5] add base structure: Report.java, CsvDocument.java --- .gitignore | 1 + src/main/java/org/writer/CsvDocument.java | 10 ++++++++++ src/main/java/org/writer/Main.java | 5 ++++- src/main/java/org/writer/Report.java | 14 ++++++++++++++ 4 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/writer/CsvDocument.java create mode 100644 src/main/java/org/writer/Report.java diff --git a/.gitignore b/.gitignore index 5ff6309..c4cc698 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ target/ !**/src/test/**/target/ ### IntelliJ IDEA ### +.idea/* .idea/modules.xml .idea/jarRepositories.xml .idea/compiler.xml diff --git a/src/main/java/org/writer/CsvDocument.java b/src/main/java/org/writer/CsvDocument.java new file mode 100644 index 0000000..08ef97b --- /dev/null +++ b/src/main/java/org/writer/CsvDocument.java @@ -0,0 +1,10 @@ +package org.writer; + +import java.util.List; + +public class CsvDocument implements Writable{ + @Override + public void writeToFile(List data, String fileName) { + System.out.println("write object as CSV"); + } +} diff --git a/src/main/java/org/writer/Main.java b/src/main/java/org/writer/Main.java index 40e8213..fa2e9b2 100644 --- a/src/main/java/org/writer/Main.java +++ b/src/main/java/org/writer/Main.java @@ -1,7 +1,10 @@ package org.writer; +import java.util.Collections; + public class Main { public static void main(String[] args) { - System.out.println("Hello world!"); + var docWriter = new Report(new CsvDocument()); + docWriter.writeObjectAsDoc(Collections.emptyList(), "People"); } } \ No newline at end of file diff --git a/src/main/java/org/writer/Report.java b/src/main/java/org/writer/Report.java new file mode 100644 index 0000000..f09e936 --- /dev/null +++ b/src/main/java/org/writer/Report.java @@ -0,0 +1,14 @@ +package org.writer; + +import lombok.RequiredArgsConstructor; + +import java.util.List; + +@RequiredArgsConstructor +public class Report { + private final Writable writable; + + public void writeObjectAsDoc(List data, String fileName) { + writable.writeToFile(data, fileName); + } +} From 4a9c2d4e16647ede1312e4d8b1d56b75c41f3183 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Fri, 17 Oct 2025 16:24:42 +0300 Subject: [PATCH 2/5] add basic implementation --- .gitignore | 3 + src/main/java/org/writer/CsvDocument.java | 10 --- src/main/java/org/writer/Main.java | 17 ++++- src/main/java/org/writer/csv/CsvDocument.java | 74 +++++++++++++++++++ .../org/writer/csv/annotations/Transient.java | 11 +++ src/main/java/org/writer/model/Person.java | 3 + 6 files changed, 105 insertions(+), 13 deletions(-) delete mode 100644 src/main/java/org/writer/CsvDocument.java create mode 100644 src/main/java/org/writer/csv/CsvDocument.java create mode 100644 src/main/java/org/writer/csv/annotations/Transient.java diff --git a/.gitignore b/.gitignore index c4cc698..1c6a0a7 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,9 @@ target/ !**/src/main/**/target/ !**/src/test/**/target/ +### files ## +.csv + ### IntelliJ IDEA ### .idea/* .idea/modules.xml diff --git a/src/main/java/org/writer/CsvDocument.java b/src/main/java/org/writer/CsvDocument.java deleted file mode 100644 index 08ef97b..0000000 --- a/src/main/java/org/writer/CsvDocument.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.writer; - -import java.util.List; - -public class CsvDocument implements Writable{ - @Override - public void writeToFile(List data, String fileName) { - System.out.println("write object as CSV"); - } -} diff --git a/src/main/java/org/writer/Main.java b/src/main/java/org/writer/Main.java index fa2e9b2..cab3a80 100644 --- a/src/main/java/org/writer/Main.java +++ b/src/main/java/org/writer/Main.java @@ -1,10 +1,21 @@ package org.writer; -import java.util.Collections; +import org.writer.csv.CsvDocument; +import org.writer.model.Months; +import org.writer.model.Person; + +import java.util.List; public class Main { public static void main(String[] args) { - var docWriter = new Report(new CsvDocument()); - docWriter.writeObjectAsDoc(Collections.emptyList(), "People"); + var report = new Report(new CsvDocument()); + report.writeObjectAsDoc(List.of(Person.builder() + .firstName("Mike") + .lastName("Petrov") + .dayOfBirth(10) + .monthOfBirth(Months.AUGUST) + .yearOfBirth(1988) + .password("myPassword") + .build()), "People"); } } \ No newline at end of file diff --git a/src/main/java/org/writer/csv/CsvDocument.java b/src/main/java/org/writer/csv/CsvDocument.java new file mode 100644 index 0000000..cf11964 --- /dev/null +++ b/src/main/java/org/writer/csv/CsvDocument.java @@ -0,0 +1,74 @@ +package org.writer.csv; + +import org.writer.Writable; +import org.writer.csv.annotations.Transient; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +public class CsvDocument implements Writable { + private static final String EXTENSION = ".csv"; + private final Predicate skipTransient = field -> !field.isAnnotationPresent(Transient.class); + + @Override + public void writeToFile(List data, String fileName) { + if (data == null || data.isEmpty()) + throw new RuntimeException(""); + + String form = form(data); + Path path = Path.of(fileName + EXTENSION); + try { + Files.writeString(path, form); + } catch (IOException e) { + throw new RuntimeException("Something went wrong!!"); + } + + } + + public List columns(T instance) { + Class metadata = instance.getClass(); + return Arrays.stream(metadata.getDeclaredFields()) + .filter(skipTransient) + .map(Field::getName) + .collect(Collectors.toList()); + } + + public List values(T instance) { + Class metadata = instance.getClass(); + List values = new ArrayList<>(); + Field[] fields = Arrays.stream(metadata.getDeclaredFields()) + .filter(skipTransient) + .toArray(Field[]::new); + for (Field field : fields) { + field.setAccessible(true); + try { + values.add(field.get(instance).toString()); + } catch (IllegalAccessException e) { + throw new RuntimeException("Access denied!"); + } + } + return values; + } + + private String form(List data) { + StringBuilder sb = new StringBuilder(); + + columns(data.get(0)).forEach(i -> sb.append(i).append(", ")); + sb.setLength(sb.length() - 2); + sb.append('\n'); + + for (int i = 0; i < data.size(); i++) { + values(data.get(0)).forEach(v -> sb.append(v).append(", ")); + sb.setLength(sb.length() - 2); + sb.append('\n'); + } + return sb.toString(); + } +} diff --git a/src/main/java/org/writer/csv/annotations/Transient.java b/src/main/java/org/writer/csv/annotations/Transient.java new file mode 100644 index 0000000..5d89993 --- /dev/null +++ b/src/main/java/org/writer/csv/annotations/Transient.java @@ -0,0 +1,11 @@ +package org.writer.csv.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface Transient { +} diff --git a/src/main/java/org/writer/model/Person.java b/src/main/java/org/writer/model/Person.java index ba8576b..c640ddb 100644 --- a/src/main/java/org/writer/model/Person.java +++ b/src/main/java/org/writer/model/Person.java @@ -3,6 +3,7 @@ import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import org.writer.csv.annotations.Transient; @Data @Builder @@ -18,5 +19,7 @@ public class Person { private Months monthOfBirth; private int yearOfBirth; + @Transient + private String password; } From 642481a689f4e86ba31a8c17e4ad73148a43f0d4 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Fri, 17 Oct 2025 20:06:37 +0300 Subject: [PATCH 3/5] add javaDoc --- src/main/java/org/writer/Report.java | 27 +++++++++ src/main/java/org/writer/csv/CsvDocument.java | 56 +++++++++++++++++-- .../org/writer/csv/annotations/Transient.java | 25 +++++++++ 3 files changed, 103 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/writer/Report.java b/src/main/java/org/writer/Report.java index f09e936..04bb9bf 100644 --- a/src/main/java/org/writer/Report.java +++ b/src/main/java/org/writer/Report.java @@ -4,10 +4,37 @@ import java.util.List; +/** + * Класс-обёртка над {@link Writable}, предоставляющий удобный метод + * для записи коллекции объектов в документ. + *

+ * Использует внедрённую стратегию {@link Writable}, что позволяет + * подменять реализацию (например, запись в CSV, JSON, XML и т.д.) + * без изменения кода клиента. + *

+ * + *

Пример использования:

+ *
{@code
+ * Writable csvWriter = new CsvDocument();
+ * Report report = new Report(csvWriter);
+ * report.writeObjectAsDoc(List.of(new Person("Alice", 30)), "people");
+ * }
+ */ @RequiredArgsConstructor public class Report { + + /** + * Стратегия записи, определяющая формат выходного документа. + */ private final Writable writable; + /** + * Записывает список объектов в документ, используя внедрённую стратегию {@link Writable}. + * + * @param data список объектов для сериализации + * @param fileName имя файла (без расширения, если реализация добавляет его сама) + * @throws RuntimeException если произошла ошибка при записи + */ public void writeObjectAsDoc(List data, String fileName) { writable.writeToFile(data, fileName); } diff --git a/src/main/java/org/writer/csv/CsvDocument.java b/src/main/java/org/writer/csv/CsvDocument.java index cf11964..dafefd5 100644 --- a/src/main/java/org/writer/csv/CsvDocument.java +++ b/src/main/java/org/writer/csv/CsvDocument.java @@ -13,25 +13,52 @@ import java.util.function.Predicate; import java.util.stream.Collectors; +/** + * Реализация интерфейса {@link Writable}, позволяющая сериализовать список объектов в CSV-файл. + *

+ * Класс использует рефлексию для извлечения имён полей и их значений. + * Поля, помеченные аннотацией {@link Transient}, игнорируются при записи. + *

+ */ public class CsvDocument implements Writable { + + /** Расширение файлов по умолчанию. */ private static final String EXTENSION = ".csv"; + + /** + * Предикат для фильтрации полей, исключающий те, что помечены аннотацией {@link Transient}. + */ private final Predicate skipTransient = field -> !field.isAnnotationPresent(Transient.class); + /** + * Записывает список объектов в CSV-файл. + * + * @param data список объектов для сериализации + * @param fileName имя файла без расширения (расширение {@code .csv} добавляется автоматически) + * @throws RuntimeException если список пустой, равен {@code null} или произошла ошибка записи + */ @Override public void writeToFile(List data, String fileName) { if (data == null || data.isEmpty()) - throw new RuntimeException(""); + throw new RuntimeException("Data list is null or empty!"); String form = form(data); Path path = Path.of(fileName + EXTENSION); try { Files.writeString(path, form); } catch (IOException e) { - throw new RuntimeException("Something went wrong!!"); + throw new RuntimeException("Something went wrong while writing CSV file!", e); } - } + /** + * Извлекает список имён полей объекта (колонки CSV). + * Поля с аннотацией {@link Transient} исключаются. + * + * @param instance объект, чьи поля анализируются + * @param тип объекта + * @return список имён полей + */ public List columns(T instance) { Class metadata = instance.getClass(); return Arrays.stream(metadata.getDeclaredFields()) @@ -40,6 +67,15 @@ public List columns(T instance) { .collect(Collectors.toList()); } + /** + * Извлекает список значений полей объекта. + * Поля с аннотацией {@link Transient} исключаются. + * + * @param instance объект, чьи значения полей извлекаются + * @param тип объекта + * @return список строковых представлений значений + * @throws RuntimeException если доступ к полю невозможен + */ public List values(T instance) { Class metadata = instance.getClass(); List values = new ArrayList<>(); @@ -51,21 +87,31 @@ public List values(T instance) { try { values.add(field.get(instance).toString()); } catch (IllegalAccessException e) { - throw new RuntimeException("Access denied!"); + throw new RuntimeException("Access denied!", e); } } return values; } + /** + * Формирует строковое представление CSV-документа. + * Первая строка содержит заголовки (имена полей), + * последующие строки — значения объектов. + * + * @param data список объектов для сериализации + * @return строка в формате CSV + */ private String form(List data) { StringBuilder sb = new StringBuilder(); + // Заголовки columns(data.get(0)).forEach(i -> sb.append(i).append(", ")); sb.setLength(sb.length() - 2); sb.append('\n'); + // Значения for (int i = 0; i < data.size(); i++) { - values(data.get(0)).forEach(v -> sb.append(v).append(", ")); + values(data.get(i)).forEach(v -> sb.append(v).append(", ")); sb.setLength(sb.length() - 2); sb.append('\n'); } diff --git a/src/main/java/org/writer/csv/annotations/Transient.java b/src/main/java/org/writer/csv/annotations/Transient.java index 5d89993..dbdd7cf 100644 --- a/src/main/java/org/writer/csv/annotations/Transient.java +++ b/src/main/java/org/writer/csv/annotations/Transient.java @@ -5,6 +5,31 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +/** + * Аннотация-маркер для исключения поля из сериализации в CSV-документ. + *

+ * Поля, помеченные этой аннотацией, будут проигнорированы при формировании + * заголовков и значений в {@code CsvDocument}. + *

+ * + *

Особенности:

+ *
    + *
  • Применяется только к полям класса.
  • + *
  • Сохраняется во время выполнения ({@link RetentionPolicy#RUNTIME}), + * что позволяет проверять её через рефлексию.
  • + *
  • Не влияет на сам объект, только на процесс сериализации.
  • + *
+ * + *

Пример использования:

+ *
{@code
+ * public class Person {
+ *     private String name;
+ *
+ *     @Transient
+ *     private String password; // не попадёт в CSV
+ * }
+ * }
+ */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface Transient { From 660e92de3cd0aefa1be7c46944d3936105d4c761 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Sun, 19 Oct 2025 13:29:24 +0300 Subject: [PATCH 4/5] add basic test --- src/main/java/org/writer/csv/CsvDocument.java | 5 +- .../java/org/writer/csv/CsvDocumentTest.java | 83 +++++++++++++++++++ .../org/writer/csv/util/TestDataBuilder.java | 26 ++++++ 3 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 src/test/java/org/writer/csv/CsvDocumentTest.java create mode 100644 src/test/java/org/writer/csv/util/TestDataBuilder.java diff --git a/src/main/java/org/writer/csv/CsvDocument.java b/src/main/java/org/writer/csv/CsvDocument.java index dafefd5..85fdeca 100644 --- a/src/main/java/org/writer/csv/CsvDocument.java +++ b/src/main/java/org/writer/csv/CsvDocument.java @@ -10,6 +10,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Objects; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -111,7 +112,9 @@ private String form(List data) { // Значения for (int i = 0; i < data.size(); i++) { - values(data.get(i)).forEach(v -> sb.append(v).append(", ")); + values(data.get(i)).stream() + .filter(Objects::nonNull) + .forEach(v -> sb.append(v).append(", ")); sb.setLength(sb.length() - 2); sb.append('\n'); } diff --git a/src/test/java/org/writer/csv/CsvDocumentTest.java b/src/test/java/org/writer/csv/CsvDocumentTest.java new file mode 100644 index 0000000..f0486c0 --- /dev/null +++ b/src/test/java/org/writer/csv/CsvDocumentTest.java @@ -0,0 +1,83 @@ +package org.writer.csv; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.writer.csv.util.TestDataBuilder; +import org.writer.model.Person; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class CsvDocumentTest { + + private final CsvDocument csvDocument = new CsvDocument(); + private final Path testFile = Path.of("test.csv"); + + @AfterEach + void cleanup() throws IOException { + Files.deleteIfExists(testFile); + } + + @Test + void testColumnsExcludesTransient() { + Person p = TestDataBuilder.person(); + List columns = csvDocument.columns(p); + + assertTrue(columns.contains("firstName")); + assertTrue(columns.contains("lastName")); + assertTrue(columns.contains("dayOfBirth")); + + // должно быть исключено + assertFalse(columns.contains("password")); + } + + @Test + void testValuesExcludesTransient() { + Person p = TestDataBuilder.person(); + List values = csvDocument.values(p); + + assertTrue(values.contains(p.getFirstName())); + assertTrue(values.contains(p.getLastName())); + // пароль не должен попасть + assertFalse(values.contains(p.getPassword())); + } + + @Test + void testWriteToFileCreatesCsv() throws IOException { + List people = TestDataBuilder.people(); + + csvDocument.writeToFile(people, "test"); + + assertTrue(Files.exists(testFile)); + + String content = Files.readString(testFile); + // Проверяем заголовки + assertTrue(content.startsWith("firstName, lastName, dayOfBirth")); + // Проверяем значения + Person p1 = people.get(0); + assertTrue(content.contains(String.format("%s, %s", p1.getFirstName(), p1.getLastName()))); + Person p2 = people.get(1); + assertTrue(content.contains(String.format("%s, %s", p2.getFirstName(), p2.getLastName()))); + // Убеждаемся, что паролей нет + assertFalse(content.contains("secret")); + assertFalse(content.contains("hidden")); + } + + @Test + void testWriteToFileThrowsOnEmptyList() { + assertThrows(RuntimeException.class, + () -> csvDocument.writeToFile(List.of(), "empty")); + } + + @Test + void testWriteToFileThrowsOnNull() { + assertThrows(RuntimeException.class, + () -> csvDocument.writeToFile(null, "null")); + } +} \ No newline at end of file diff --git a/src/test/java/org/writer/csv/util/TestDataBuilder.java b/src/test/java/org/writer/csv/util/TestDataBuilder.java new file mode 100644 index 0000000..6e13409 --- /dev/null +++ b/src/test/java/org/writer/csv/util/TestDataBuilder.java @@ -0,0 +1,26 @@ +package org.writer.csv.util; + +import org.writer.model.Months; +import org.writer.model.Person; + +import java.util.List; + +public class TestDataBuilder { + public static Person person() { + return Person.builder() + .firstName("Ivan") + .lastName("Petrov") + .dayOfBirth(10) + .monthOfBirth(Months.AUGUST) + .yearOfBirth(1988) + .password("secret") + .build(); + } + + public static List people() { + return List.of( + Person.builder().firstName("Nina").lastName("Ivanova").dayOfBirth(10).monthOfBirth(Months.AUGUST).yearOfBirth(1994).password("secret").build(), + Person.builder().firstName("Bob").lastName("Smith").dayOfBirth(17).monthOfBirth(Months.JULY).yearOfBirth(1987).password("hidden").build() + ); + } +} From 37ea30b267b83f6dd9e3b293e2fcd44b579b5a67 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Sun, 19 Oct 2025 15:32:07 +0300 Subject: [PATCH 5/5] change method form() --- src/main/java/org/writer/csv/CsvDocument.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/main/java/org/writer/csv/CsvDocument.java b/src/main/java/org/writer/csv/CsvDocument.java index 85fdeca..dafefd5 100644 --- a/src/main/java/org/writer/csv/CsvDocument.java +++ b/src/main/java/org/writer/csv/CsvDocument.java @@ -10,7 +10,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.Objects; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -112,9 +111,7 @@ private String form(List data) { // Значения for (int i = 0; i < data.size(); i++) { - values(data.get(i)).stream() - .filter(Objects::nonNull) - .forEach(v -> sb.append(v).append(", ")); + values(data.get(i)).forEach(v -> sb.append(v).append(", ")); sb.setLength(sb.length() - 2); sb.append('\n'); }