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
103 changes: 83 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,83 @@
# Практическое задание 1 # CSV Writer

Мы пишем приложение, которое должно генерировать отчеты в разных форматах и нам хочется генерировать какой-нибудь отчёт в CSV, но проблема в том, что у нас нет такого инструмента. Поэтому мы решаем, что нам нужна библиотека для генерации CSV файлов.
Реализация может быть построена с помощью Reflection и аннотаций.

Папка model содержит классы, которые нужно сохранять в CSV. Классы можно изменять и добавлять новые.

**Описание:**<br>
✅ Написать реализацию Writable, которая будет сохранять объекты в CSV формате в файл<br>
✅ Написать unit тесты<br>
✅ Написать JavaDoc<br>
✅ Для генерации тестовых данных можно использовать Datafaker<br>
✅ В main должно быть несколько примеров сохранения данных в файл

**Как выполнить:**
1. Сделать форк этого проекта
2. Сделать ветку dev в своем проекте. Выполнять работу в ней
3. После выполнения работы открыть Pull Request в master
4. Назначить ментора ревьювером
5. Отправить ссылку на PR в тг чат ментору
# CSV Writer

Кастомная библиотека для генерации CSV файлов из Java объектов.

## Описание

Простая и гибкая реализация для экспорта Java объектов в CSV формат. Использует аннотации для конфигурации полей и рефлексию для извлечения данных.

## Реализованная функциональность

### Основные компоненты:

- **`@CsvColumn`** - аннотация для маркировки полей
- `name` - имя колонки в CSV
- `order` - порядок следования колонок

- **`CsvWriter`** - основной класс для записи в CSV
- Автоматическое создание заголовков
- Сортировка колонок по order
- Экранирование специальных символов
- Поддержка коллекций (объединение через `;`)
- Поддержка enum и null значений

- **Модели данных**
- `Person` - информация о человеке (имя, фамилия, дата рождения)
- `Student` - студент с оценками
- `Months` - enum для месяцев

## Использование

```java
CsvWriter writer = new CsvWriter();

// Создаем данные
List<Person> people = Arrays.asList(
Person.builder()
.firstName("Иван")
.lastName("Иванов")
.dayOfBirth(15)
.monthOfBirth(Months.MARCH)
.yearOfBirth(1990)
.build()
);

// Записываем в файл
writer.writeToFile(people, "people.csv");
```

## Покрытие тестами

**16 юнит и интеграционных тестов**, включая:

### Базовая функциональность:
- Запись простых объектов и коллекций
- Экранирование специальных символов (`,`, `"`, переносы строк)
- Обработка null значений
- Валидация входных данных
- Работа с enum
- Кастомные имена и порядок колонок

### Тесты с Datafaker:
- Генерация больших наборов данных (100, 1000 записей)
- Тест производительности

## Примеры вывода

### people.csv
```csv
First Name,Last Name,Day,Month,Year
Иван,Иванов,15,MARCH,1990
Мария,Смирнова,23,JULY,1985
```

### students.csv
```csv
Student Name,Scores
Алиса Иванова,95;88;92;90
Борис Сидоров,78;85;80;82
```

## Документация

Все классы и методы содержат JavaDoc с описанием использования.
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@
<version>5.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.datafaker</groupId>
<artifactId>datafaker</artifactId>
<version>2.5.2</version>
<scope>test</scope>
</dependency>
</dependencies>

</project>
153 changes: 153 additions & 0 deletions src/main/java/org/writer/CsvWriter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package org.writer;

import org.writer.annotation.CsvColumn;

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.*;
import java.util.stream.Collectors;

/**
* Реализация интерфейса Writable для записи объектов в CSV формат.
* Использует рефлексию для извлечения значений полей из объектов и аннотации для настройки вывода.
*/
public class CsvWriter implements Writable {

private static final String DELIMITER = ",";
private static final String LINE_SEPARATOR = System.lineSeparator();

/**
* Записывает список объектов в CSV файл.
* В выходной файл будут включены только поля, аннотированные @CsvColumn.
*
* @param data список объектов для записи
* @param fileName имя выходного файла
* @throws IllegalArgumentException если data равен null или пустой
* @throws RuntimeException если произошла ошибка при записи файла
*/
@Override
public void writeToFile(List<?> data, String fileName) {
if (data == null || data.isEmpty()) {
throw new IllegalArgumentException("Data list cannot be null or empty");
}

Class<?> clazz = data.get(0).getClass();
List<FieldInfo> fields = getAnnotatedFields(clazz);

if (fields.isEmpty()) {
throw new IllegalArgumentException("No @CsvColumn annotated fields found in class " + clazz.getName());
}

try (BufferedWriter writer = new BufferedWriter(new FileWriter(fileName))) {
// Записываем заголовок
String header = fields.stream()
.map(FieldInfo::columnName)
.collect(Collectors.joining(DELIMITER));
writer.write(header);
writer.write(LINE_SEPARATOR);

// Записываем строки данных
for (Object obj : data) {
String row = fields.stream()
.map(fieldInfo -> getFieldValue(obj, fieldInfo.field()))
.map(this::formatValue)
.collect(Collectors.joining(DELIMITER));
writer.write(row);
writer.write(LINE_SEPARATOR);
}
} catch (IOException e) {
throw new RuntimeException("Error writing to file: " + fileName, e);
}
}

/**
* Получает все поля, аннотированные @CsvColumn, из класса.
* Поля сортируются по атрибуту order.
*
* @param clazz класс для анализа
* @return список информации о полях, отсортированный по order
*/
private List<FieldInfo> getAnnotatedFields(Class<?> clazz) {
List<FieldInfo> fieldInfos = new ArrayList<>();

for (Field field : clazz.getDeclaredFields()) {
if (field.isAnnotationPresent(CsvColumn.class)) {
CsvColumn annotation = field.getAnnotation(CsvColumn.class);
String columnName = annotation.name().isEmpty() ? field.getName() : annotation.name();
fieldInfos.add(new FieldInfo(field, columnName, annotation.order()));
}
}

// Сортируем по order
fieldInfos.sort(Comparator.comparingInt(FieldInfo::order));
return fieldInfos;
}

/**
* Извлекает значение поля из объекта с использованием рефлексии.
*
* @param obj объект, из которого извлекается значение
* @param field поле для извлечения
* @return значение поля, или null если извлечение не удалось
*/
private Object getFieldValue(Object obj, Field field) {
try {
field.setAccessible(true);
return field.get(obj);
} catch (IllegalAccessException e) {
throw new RuntimeException("Error accessing field: " + field.getName(), e);
}
}

/**
* Форматирует значение для вывода в CSV.
* Обрабатывает null значения, коллекции и специальные символы, требующие экранирования.
*
* @param value значение для форматирования
* @return отформатированное строковое значение
*/
private String formatValue(Object value) {
if (value == null) {
return "";
}

// Обрабатываем коллекции
if (value instanceof Collection<?> collection) {
String joined = collection.stream()
.map(Object::toString)
.collect(Collectors.joining(";"));
return escapeValue(joined);
}

// Обрабатываем enum
if (value instanceof Enum<?>) {
return value.toString();
}

return escapeValue(value.toString());
}

/**
* Экранирует специальные символы в CSV значениях.
* Оборачивает значения в кавычки, если они содержат запятые, кавычки или переносы строк.
*
* @param value значение для экранирования
* @return экранированное значение
*/
private String escapeValue(String value) {
if (value.contains(",") || value.contains("\"") || value.contains("\n") || value.contains("\r")) {
// Экранируем кавычки путем их удвоения
String escaped = value.replace("\"", "\"\"");
return "\"" + escaped + "\"";
}
return value;
}

/**
* Внутренний класс для хранения метаданных поля.
*/
private record FieldInfo(Field field, String columnName, int order) {
}
}
Loading