diff --git a/pom.xml b/pom.xml
index effc1bd..5715465 100644
--- a/pom.xml
+++ b/pom.xml
@@ -27,6 +27,11 @@
5.8.1
test
+
+ com.github.javafaker
+ javafaker
+ 1.0.2
+
\ No newline at end of file
diff --git a/src/main/java/org/writer/Main.java b/src/main/java/org/writer/Main.java
index 40e8213..031abce 100644
--- a/src/main/java/org/writer/Main.java
+++ b/src/main/java/org/writer/Main.java
@@ -1,7 +1,60 @@
package org.writer;
+
+import com.github.javafaker.Faker;
+import org.writer.model.*;
+import org.writer.utils.writers.Writable;
+import org.writer.utils.writers.csvwriter.CSVWriter;
+
+import java.io.IOException;
+import java.time.LocalDate;
+import java.util.List;
+import java.util.Locale;
+import java.util.UUID;
+
public class Main {
- public static void main(String[] args) {
- System.out.println("Hello world!");
+ public static void main(String[] args) throws IOException {
+ Faker faker = new Faker(Locale.ENGLISH);
+
+ UUID id = UUID.randomUUID();
+ String fullname = faker.name().fullName();
+ LocalDate date = faker.date().birthday(0, 10).toInstant()
+ .atZone(java.time.ZoneId.systemDefault())
+ .toLocalDate();
+
+ Employee employee = new Employee(id, fullname, date);
+ List employees = List.of(employee);
+
+ Writable writerE = new CSVWriter<>();
+
+ writerE.writeToFile(employees, "src\\main\\resources" +
+ "\\reportEmployee.csv");
+
+ Product product = new Product(UUID.randomUUID(), faker.company().name(),
+ Double.parseDouble(faker.commerce().price().replace(',', '.')));
+ List products = List.of(product);
+
+ Writable writerP = new CSVWriter<>();
+ writerP.writeToFile(products, "src\\main\\resources" +
+ "\\reportProduct");
+
+ List persons = List.of(Person.builder()
+ .firstName("Pasha")
+ .lastName("Barabaska")
+ .dayOfBirth(2)
+ .monthOfBirth(Months.JANUARY)
+ .yearOfBirth(1992)
+ .build());
+
+ Writable writerPerson = new CSVWriter<>();
+ writerPerson.writeToFile(persons, "src\\main\\resources" +
+ "\\reportPersons");
+
+ List students = List.of(Student.builder()
+ .name("bacugan")
+ .score(List.of("qwerty", "wasd", "12345")).build());
+ Writable writerStudent = new CSVWriter<>();
+ writerStudent.writeToFile(students, "src\\main\\resources" +
+ "\\reportStudents");
}
}
\ No newline at end of file
diff --git a/src/main/java/org/writer/Writable.java b/src/main/java/org/writer/Writable.java
deleted file mode 100644
index 9f60c45..0000000
--- a/src/main/java/org/writer/Writable.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package org.writer;
-
-import java.util.List;
-
-public interface Writable {
-
- void writeToFile(List> data, String fileName);
-
-}
diff --git a/src/main/java/org/writer/annotations/CSVColumn.java b/src/main/java/org/writer/annotations/CSVColumn.java
new file mode 100644
index 0000000..3723cbb
--- /dev/null
+++ b/src/main/java/org/writer/annotations/CSVColumn.java
@@ -0,0 +1,21 @@
+package org.writer.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation for marking the fields that will form the CSV report headers.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.FIELD)
+public @interface CSVColumn {
+
+ /**
+ * column name in CSV report.
+ *
+ * @return column name
+ */
+ String columnName();
+}
diff --git a/src/main/java/org/writer/annotations/CSVReport.java b/src/main/java/org/writer/annotations/CSVReport.java
new file mode 100644
index 0000000..b522611
--- /dev/null
+++ b/src/main/java/org/writer/annotations/CSVReport.java
@@ -0,0 +1,14 @@
+package org.writer.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation for marking classes that can be written to the CSV report.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface CSVReport {
+}
diff --git a/src/main/java/org/writer/model/Employee.java b/src/main/java/org/writer/model/Employee.java
new file mode 100644
index 0000000..2382541
--- /dev/null
+++ b/src/main/java/org/writer/model/Employee.java
@@ -0,0 +1,13 @@
+package org.writer.model;
+
+import org.writer.annotations.CSVColumn;
+import org.writer.annotations.CSVReport;
+
+import java.time.LocalDate;
+import java.util.UUID;
+
+@CSVReport
+public record Employee(@CSVColumn(columnName = "id") UUID id,
+ @CSVColumn(columnName = "full name of the employee") String fullname,
+ @CSVColumn(columnName = "date of employment") LocalDate dateOfEmployment ) {
+}
diff --git a/src/main/java/org/writer/model/Person.java b/src/main/java/org/writer/model/Person.java
index ba8576b..1b8a79d 100644
--- a/src/main/java/org/writer/model/Person.java
+++ b/src/main/java/org/writer/model/Person.java
@@ -3,20 +3,28 @@
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
+import org.writer.annotations.CSVColumn;
+import org.writer.annotations.CSVReport;
@Data
@Builder
@AllArgsConstructor
+@CSVReport
public class Person {
+ @CSVColumn(columnName = "first name")
private String firstName;
+ @CSVColumn(columnName = "last name")
private String lastName;
+ @CSVColumn(columnName = "day of birth")
private int dayOfBirth;
+ @CSVColumn(columnName = "month of birth")
private Months monthOfBirth;
+ @CSVColumn(columnName = "year of birth")
private int yearOfBirth;
}
diff --git a/src/main/java/org/writer/model/Product.java b/src/main/java/org/writer/model/Product.java
new file mode 100644
index 0000000..b74f3e2
--- /dev/null
+++ b/src/main/java/org/writer/model/Product.java
@@ -0,0 +1,12 @@
+package org.writer.model;
+
+import org.writer.annotations.CSVColumn;
+import org.writer.annotations.CSVReport;
+
+import java.util.UUID;
+
+@CSVReport
+public record Product(@CSVColumn(columnName = "id") UUID id,
+ @CSVColumn(columnName = "product name") String name,
+ @CSVColumn(columnName = "price in dollars") double price) {
+}
diff --git a/src/main/java/org/writer/model/Student.java b/src/main/java/org/writer/model/Student.java
index 2b549c4..906a26f 100644
--- a/src/main/java/org/writer/model/Student.java
+++ b/src/main/java/org/writer/model/Student.java
@@ -3,15 +3,20 @@
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
+import org.writer.annotations.CSVColumn;
+import org.writer.annotations.CSVReport;
import java.util.List;
@Data
@Builder
@AllArgsConstructor
+@CSVReport
public class Student {
+ @CSVColumn(columnName = "name")
private String name;
+ @CSVColumn(columnName = "score")
private List score;
}
\ No newline at end of file
diff --git a/src/main/java/org/writer/utils/FileType.java b/src/main/java/org/writer/utils/FileType.java
new file mode 100644
index 0000000..d0001bd
--- /dev/null
+++ b/src/main/java/org/writer/utils/FileType.java
@@ -0,0 +1,35 @@
+package org.writer.utils;
+
+import lombok.Getter;
+
+/**
+ * Enumeration of supported file formats for data export.
+ * Each format contains file extension information.
+ */
+@Getter
+public enum FileType {
+
+ /**
+ * Comma-Separated Values format.
+ * Text format for representing tabular data.
+ */
+ CSV(".csv");
+
+ /**
+ * -- GETTER --
+ * Returns the file extension for this format.
+ *
+ * @return file extension string including the dot.
+ */
+ private final String fileFormat;
+
+ /**
+ * Creates a file type with the specified extension.
+ *
+ * @param fileFormat file extension.
+ */
+ FileType(String fileFormat) {
+ this.fileFormat = fileFormat;
+ }
+
+}
diff --git a/src/main/java/org/writer/utils/FileUtil.java b/src/main/java/org/writer/utils/FileUtil.java
new file mode 100644
index 0000000..71b280a
--- /dev/null
+++ b/src/main/java/org/writer/utils/FileUtil.java
@@ -0,0 +1,31 @@
+package org.writer.utils;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
+
+/**
+ * Utility class for guaranteed writing of a list of data to a file.
+ */
+public class FileUtil {
+
+ /**
+ * Static method for writing a list of data to a file at a specified absolute address.
+ * If the address or file does not exist, it will be created.
+ * Truncates the file if it already exists.
+ *
+ * @param filePath Absolute address of the file to write to.
+ * @param lines The data lines that will be written to the file.
+ * @throws IOException If an I/O error occurs while writing to the file.
+ */
+ public static void writeLinesToFile(String filePath, Iterable lines) throws IOException {
+ Path path = Paths.get(filePath);
+ if (Files.notExists(path)){
+ Files.createDirectories(path.getParent());
+ Files.createFile(path);
+ }
+ Files.write(path, lines, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
+ }
+}
diff --git a/src/main/java/org/writer/utils/writers/AbstractWriter.java b/src/main/java/org/writer/utils/writers/AbstractWriter.java
new file mode 100644
index 0000000..864aa6c
--- /dev/null
+++ b/src/main/java/org/writer/utils/writers/AbstractWriter.java
@@ -0,0 +1,85 @@
+package org.writer.utils.writers;
+
+
+import org.writer.utils.FileType;
+import org.writer.utils.FileUtil;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Abstract class for writing data to files of various formats.
+ * Provides a general implementation for working with annotated fields.
+ *
+ * @param type of data being written
+ */
+public abstract class AbstractWriter implements Writable{
+
+ /**
+ * Returns the file type for the current implementation.
+ *
+ * @return file type
+ */
+ protected abstract FileType getFileType();
+
+ /**
+ * Gets a list of fields suitable for export to the current format.
+ *
+ * @param clazz class to parse
+ * @return list of fields to export
+ */
+ protected abstract List getExportFields(Class> clazz);
+
+ /**
+ * Converts an object to a string for writing to a file.
+ *
+ * @param obj the object to convert
+ * @param fields the list of fields to process
+ * @return the string representation of the object
+ */
+ protected abstract String formatRow(T obj, List fields);
+
+ /**
+ * Generates file headers.
+ *
+ * @param fields list of fields
+ * @return string with headers
+ */
+ protected abstract String formatHeaders(List fields);
+
+ @Override
+ public void writeToFile(List data, String path) throws IOException {
+ if (data.isEmpty()) {
+ throw new IllegalArgumentException("Data is empty!");
+ }
+
+ Class> clazz = data.get(0).getClass();
+ List fields = getExportFields(clazz);
+
+ List report = new ArrayList<>();
+ report.add(formatHeaders(fields));
+
+ for (T item : data) {
+ report.add(formatRow(item, fields));
+ }
+
+ String fullPath = ensureFileExtension(path, getFileType());
+ FileUtil.writeLinesToFile(fullPath, report);
+ }
+
+ /**
+ * Adds file extension if it is missing.
+ *
+ * @param path file path
+ * @param fileType file type
+ * @return path with extension
+ */
+ protected String ensureFileExtension(String path, FileType fileType) {
+ if (!path.endsWith(fileType.getFileFormat())) {
+ return path + fileType.getFileFormat();
+ }
+ return path;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/writer/utils/writers/Writable.java b/src/main/java/org/writer/utils/writers/Writable.java
new file mode 100644
index 0000000..8d6b057
--- /dev/null
+++ b/src/main/java/org/writer/utils/writers/Writable.java
@@ -0,0 +1,21 @@
+package org.writer.utils.writers;
+
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * Interface for writing file data.
+ *
+ * @param Record data type.
+ */
+public interface Writable {
+
+ /**
+ * Method for writing report file.
+ *
+ * @param data List of data to record.
+ * @param path Absolute address of the file to write to.
+ * @throws IOException If an I/O error occurs while writing to the file.
+ */
+ void writeToFile(List data, String path) throws IOException;
+}
diff --git a/src/main/java/org/writer/utils/writers/csvwriter/CSVWriter.java b/src/main/java/org/writer/utils/writers/csvwriter/CSVWriter.java
new file mode 100644
index 0000000..251a114
--- /dev/null
+++ b/src/main/java/org/writer/utils/writers/csvwriter/CSVWriter.java
@@ -0,0 +1,78 @@
+package org.writer.utils.writers.csvwriter;
+
+
+import org.writer.annotations.CSVColumn;
+import org.writer.annotations.CSVReport;
+import org.writer.utils.FileType;
+import org.writer.utils.writers.AbstractWriter;
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Implementation of writing data to CSV format.
+ *
+ * @param type of written data
+ */
+public class CSVWriter extends AbstractWriter {
+ @Override
+ protected FileType getFileType() {
+ return FileType.CSV;
+ }
+
+ @Override
+ protected List getExportFields(Class> clazz) {
+ if (!clazz.isAnnotationPresent(CSVReport.class)) {
+ throw new IllegalArgumentException("Data model is not suitable for CSV export!");
+ }
+
+ List fields = new ArrayList<>();
+ for (Field field : clazz.getDeclaredFields()) {
+ if (field.isAnnotationPresent(CSVColumn.class)) {
+ fields.add(field);
+ }
+ }
+ return fields;
+ }
+
+ @Override
+ protected String formatHeaders(List fields) {
+ StringBuilder headers = new StringBuilder();
+ Iterator iterator = fields.iterator();
+
+ while (iterator.hasNext()) {
+ Field field = iterator.next();
+ CSVColumn annotation = field.getAnnotation(CSVColumn.class);
+ headers.append(annotation.columnName());
+ if (iterator.hasNext()) {
+ headers.append(",");
+ }
+ }
+ return headers.toString();
+ }
+
+ @Override
+ protected String formatRow(T obj, List fields) {
+ StringBuilder row = new StringBuilder();
+ Iterator iterator = fields.iterator();
+
+ while (iterator.hasNext()) {
+ Field field = iterator.next();
+ field.setAccessible(true);
+ try {
+ Object value = field.get(obj);
+ String stringValue = (value != null) ? value.toString() : "";
+ row.append(stringValue);
+ if (iterator.hasNext()) {
+ row.append(",");
+ }
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException("Cannot access field: " + field.getName(), e);
+ }
+ }
+
+ return row.toString();
+ }
+}
diff --git a/src/test/java/utils/FileUtilsTest.java b/src/test/java/utils/FileUtilsTest.java
new file mode 100644
index 0000000..391dc2c
--- /dev/null
+++ b/src/test/java/utils/FileUtilsTest.java
@@ -0,0 +1,62 @@
+package utils;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.writer.utils.FileUtil;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+
+public class FileUtilsTest {
+
+ @TempDir
+ static Path tempDir;
+ static List teslList;
+
+ @BeforeAll
+ public static void beforeAll() {
+ teslList = List.of("qwerty", "12345");
+ }
+
+ @Test
+ @DisplayName("Write data to an existing file.")
+ public void testWriteToExistFile() throws IOException {
+ Path path = tempDir.resolve("exist.txt");
+ Files.createFile(path);
+ FileUtil.writeLinesToFile(path.toString(), teslList);
+
+ List result = Files.readAllLines(path);
+ Assertions.assertEquals(teslList, result, "the data of the files must match.");
+ }
+
+ @Test
+ @DisplayName("Create file if it does not exist.")
+ public void testCreateFile() throws IOException {
+ Path path = tempDir.resolve("notExist.txt");
+ FileUtil.writeLinesToFile(path.toString(), teslList);
+
+ boolean result = Files.exists(path);
+ Assertions.assertTrue(result, "The file must be created.");
+ }
+
+ @Test
+ @DisplayName("Write data to an not existing file.")
+ public void testWriteToNotExistFile() throws IOException {
+ Path path = tempDir.resolve("notExist.txt");
+ FileUtil.writeLinesToFile(path.toString(), teslList);
+
+ List result = Files.readAllLines(path);
+ Assertions.assertEquals(teslList, result, "the data of the files must match.");
+ }
+
+ @Test
+ @DisplayName("Writing to file at wrong address.")
+ public void testWriteToFileWrongAddress() {
+ Assertions.assertThrows(IOException.class, ()-> FileUtil.writeLinesToFile(tempDir.toString(), teslList));
+ }
+}
diff --git a/src/test/java/utils/writers/csvwriter/CSVWriterTest.java b/src/test/java/utils/writers/csvwriter/CSVWriterTest.java
new file mode 100644
index 0000000..cb1eb98
--- /dev/null
+++ b/src/test/java/utils/writers/csvwriter/CSVWriterTest.java
@@ -0,0 +1,146 @@
+package utils.writers.csvwriter;
+
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.writer.annotations.CSVColumn;
+import org.writer.annotations.CSVReport;
+import org.writer.utils.writers.Writable;
+import org.writer.utils.writers.csvwriter.CSVWriter;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Collections;
+import java.util.List;
+
+public class CSVWriterTest {
+
+ @TempDir
+ static Path tempDir;
+
+ static Writable withAnnotationWritable;
+ static Writable withoutAnnotationWritable;
+
+ static List withAnnotationList;
+ static List withoutAnnotationList;
+
+ @BeforeAll
+ static void beforeAll() {
+ withAnnotationWritable = new CSVWriter<>();
+ withoutAnnotationWritable = new CSVWriter<>();
+ withAnnotationList = List.of(
+ new TestClassWithAnnotation("A", "B", "C"),
+ new TestClassWithAnnotation("D", "E", "F")
+ );
+ withoutAnnotationList = List.of(
+ new TestClassWithoutAnnotation("A", "B", "C"),
+ new TestClassWithoutAnnotation("D", "E", "F")
+ );
+ }
+
+ @Test
+ @DisplayName("Write data to valid file path.")
+ public void testWriteToValidPath() throws IOException {
+ Path filePath = tempDir.resolve("valid_test.csv");
+ withAnnotationWritable.writeToFile(withAnnotationList, filePath.toString());
+
+ List result = Files.readAllLines(filePath);
+ Assertions.assertEquals(3, result.size());
+ Assertions.assertEquals("column 1,column 2", result.get(0));
+ Assertions.assertEquals("A,B", result.get(1));
+ Assertions.assertEquals("D,E", result.get(2));
+ }
+
+ @Test
+ @DisplayName("Write data with empty path.")
+ public void testWriteWithEmptyPath() {
+ String emptyPath = "";
+
+ Assertions.assertThrows(NullPointerException.class, () ->
+ withAnnotationWritable.writeToFile(withAnnotationList, emptyPath)
+ );
+ }
+
+ @Test
+ @DisplayName("Write data with path is null.")
+ public void testWriteWithPathIsNull() {
+ String nullPath = null;
+
+ Assertions.assertThrows(NullPointerException.class, () ->
+ withAnnotationWritable.writeToFile(withAnnotationList, nullPath)
+ );
+ }
+
+ @Test
+ @DisplayName("Write class without @CSVReport annotation.")
+ public void testWriteClassWithoutAnnotation() {
+ String path = tempDir.resolve("no_annotation.csv").toString();
+
+ Assertions.assertThrows(IllegalArgumentException.class, () ->
+ withoutAnnotationWritable.writeToFile(withoutAnnotationList, path)
+ );
+ }
+
+ @Test
+ @DisplayName("Write null data.")
+ public void testWriteNullData() {
+ String path = tempDir.resolve("null_data.csv").toString();
+ List nullData = null;
+
+ Assertions.assertThrows(NullPointerException.class, () ->
+ withAnnotationWritable.writeToFile(nullData, path)
+ );
+ }
+
+ @Test
+ @DisplayName("Write empty data.")
+ public void testWriteEmptyData() {
+ String path = tempDir.resolve("empty_data.csv").toString();
+ List emptyData = Collections.emptyList();
+
+ Assertions.assertThrows(IllegalArgumentException.class, () ->
+ withAnnotationWritable.writeToFile(emptyData, path)
+ );
+ }
+
+ @Test
+ @DisplayName("Check file format with correct filename.")
+ public void testCorrectFilenameFormat() throws IOException {
+ String path = tempDir.resolve("correct_name.csv").toString();
+
+ withAnnotationWritable.writeToFile(withAnnotationList, path);
+
+ Assertions.assertTrue(Paths.get(path).endsWith("correct_name.csv"));
+ }
+
+ @Test
+ @DisplayName("Check file handling with incorrect filename.")
+ public void testIncorrectFilenameFormat() throws IOException {
+ String path = tempDir.resolve("incorrect_name").toString();
+
+ withAnnotationWritable.writeToFile(withAnnotationList, path);
+
+ Assertions.assertTrue(Files.notExists(Paths.get(path)));
+ Assertions.assertTrue(Files.exists(Paths.get(path + ".csv")));
+ }
+
+ @CSVReport
+ record TestClassWithAnnotation(
+ @CSVColumn(columnName = "column 1") String column,
+ @CSVColumn(columnName = "column 2") String column2,
+ String column3
+ ) {
+ }
+
+ record TestClassWithoutAnnotation(
+ @CSVColumn(columnName = "column 1") String column,
+ @CSVColumn(columnName = "column 2") String column2,
+ String column3
+ ) {
+ }
+}