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 + ) { + } +}