diff --git a/.gitignore b/.gitignore index 5ff6309..1c6a0a7 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,11 @@ target/ !**/src/main/**/target/ !**/src/test/**/target/ +### files ## +.csv + ### IntelliJ IDEA ### +.idea/* .idea/modules.xml .idea/jarRepositories.xml .idea/compiler.xml diff --git a/src/main/java/org/writer/Main.java b/src/main/java/org/writer/Main.java index 40e8213..cab3a80 100644 --- a/src/main/java/org/writer/Main.java +++ b/src/main/java/org/writer/Main.java @@ -1,7 +1,21 @@ package org.writer; +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) { - System.out.println("Hello world!"); + 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/Report.java b/src/main/java/org/writer/Report.java new file mode 100644 index 0000000..04bb9bf --- /dev/null +++ b/src/main/java/org/writer/Report.java @@ -0,0 +1,41 @@ +package org.writer; + +import lombok.RequiredArgsConstructor; + +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 new file mode 100644 index 0000000..dafefd5 --- /dev/null +++ b/src/main/java/org/writer/csv/CsvDocument.java @@ -0,0 +1,120 @@ +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; + +/** + * Реализация интерфейса {@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("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 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()) + .filter(skipTransient) + .map(Field::getName) + .collect(Collectors.toList()); + } + + /** + * Извлекает список значений полей объекта. + * Поля с аннотацией {@link Transient} исключаются. + * + * @param instance объект, чьи значения полей извлекаются + * @param тип объекта + * @return список строковых представлений значений + * @throws RuntimeException если доступ к полю невозможен + */ + 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!", 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(i)).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..dbdd7cf --- /dev/null +++ b/src/main/java/org/writer/csv/annotations/Transient.java @@ -0,0 +1,36 @@ +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; + +/** + * Аннотация-маркер для исключения поля из сериализации в 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 { +} 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; } 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() + ); + } +}