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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 15 additions & 1 deletion src/main/java/org/writer/Main.java
Original file line number Diff line number Diff line change
@@ -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");
}
}
41 changes: 41 additions & 0 deletions src/main/java/org/writer/Report.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package org.writer;

import lombok.RequiredArgsConstructor;

import java.util.List;

/**
* Класс-обёртка над {@link Writable}, предоставляющий удобный метод
* для записи коллекции объектов в документ.
* <p>
* Использует внедрённую стратегию {@link Writable}, что позволяет
* подменять реализацию (например, запись в CSV, JSON, XML и т.д.)
* без изменения кода клиента.
* </p>
*
* <p>Пример использования:</p>
* <pre>{@code
* Writable csvWriter = new CsvDocument();
* Report report = new Report(csvWriter);
* report.writeObjectAsDoc(List.of(new Person("Alice", 30)), "people");
* }</pre>
*/
@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);
}
}
120 changes: 120 additions & 0 deletions src/main/java/org/writer/csv/CsvDocument.java
Original file line number Diff line number Diff line change
@@ -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-файл.
* <p>
* Класс использует рефлексию для извлечения имён полей и их значений.
* Поля, помеченные аннотацией {@link Transient}, игнорируются при записи.
* </p>
*/
public class CsvDocument implements Writable {

/** Расширение файлов по умолчанию. */
private static final String EXTENSION = ".csv";

/**
* Предикат для фильтрации полей, исключающий те, что помечены аннотацией {@link Transient}.
*/
private final Predicate<Field> 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 <T> тип объекта
* @return список имён полей
*/
public <T> List<String> 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 <T> тип объекта
* @return список строковых представлений значений
* @throws RuntimeException если доступ к полю невозможен
*/
public <T> List<String> values(T instance) {
Class<?> metadata = instance.getClass();
List<String> 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();
}
}
36 changes: 36 additions & 0 deletions src/main/java/org/writer/csv/annotations/Transient.java
Original file line number Diff line number Diff line change
@@ -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-документ.
* <p>
* Поля, помеченные этой аннотацией, будут проигнорированы при формировании
* заголовков и значений в {@code CsvDocument}.
* </p>
*
* <p>Особенности:</p>
* <ul>
* <li>Применяется только к полям класса.</li>
* <li>Сохраняется во время выполнения ({@link RetentionPolicy#RUNTIME}),
* что позволяет проверять её через рефлексию.</li>
* <li>Не влияет на сам объект, только на процесс сериализации.</li>
* </ul>
*
* <p>Пример использования:</p>
* <pre>{@code
* public class Person {
* private String name;
*
* @Transient
* private String password; // не попадёт в CSV
* }
* }</pre>
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Transient {
}
3 changes: 3 additions & 0 deletions src/main/java/org/writer/model/Person.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.csv.annotations.Transient;

@Data
@Builder
Expand All @@ -18,5 +19,7 @@ public class Person {
private Months monthOfBirth;

private int yearOfBirth;
@Transient
private String password;

}
83 changes: 83 additions & 0 deletions src/test/java/org/writer/csv/CsvDocumentTest.java
Original file line number Diff line number Diff line change
@@ -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<String> 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<String> 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<Person> 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"));
}
}
26 changes: 26 additions & 0 deletions src/test/java/org/writer/csv/util/TestDataBuilder.java
Original file line number Diff line number Diff line change
@@ -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<Person> 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()
);
}
}