From f494c48e9b5e9af8604f075dd5ee866de8af7008 Mon Sep 17 00:00:00 2001 From: ysneve Date: Sat, 25 Jul 2026 20:18:36 +0500 Subject: [PATCH 1/3] Add CsvWriter --- pom.xml | 11 +- src/main/java/org/writer/Main.java | 14 +++ .../org/writer/annotation/CsvWritable.java | 15 +++ src/main/java/org/writer/model/Person.java | 6 +- src/main/java/org/writer/model/Student.java | 4 +- .../java/org/writer/service/CsvWriter.java | 103 ++++++++++++++++++ 6 files changed, 147 insertions(+), 6 deletions(-) create mode 100644 src/main/java/org/writer/annotation/CsvWritable.java create mode 100644 src/main/java/org/writer/service/CsvWriter.java diff --git a/pom.xml b/pom.xml index effc1bd..7c25bea 100644 --- a/pom.xml +++ b/pom.xml @@ -9,16 +9,21 @@ 1.0-SNAPSHOT - 17 - 17 + 21 + 21 UTF-8 + + net.datafaker + datafaker + 2.7.0 + org.projectlombok lombok - 1.18.34 + 1.18.36 provided diff --git a/src/main/java/org/writer/Main.java b/src/main/java/org/writer/Main.java index 40e8213..5a7241f 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 java.util.Arrays; +import java.util.List; +import org.writer.model.Student; +import org.writer.service.CsvWriter; + public class Main { public static void main(String[] args) { System.out.println("Hello world!"); + var writer = new CsvWriter(); + + var listOfObjects = List.of( + new Student("Sergey", Arrays.asList("A", "B")), + new Student("Sergey", null), + new Student("Alexander", List.of("A", "C"))); + writer.writeToFile(listOfObjects, "Test"); + + writer.writeToFile(List.of(1, 2), "test"); } } \ No newline at end of file diff --git a/src/main/java/org/writer/annotation/CsvWritable.java b/src/main/java/org/writer/annotation/CsvWritable.java new file mode 100644 index 0000000..742ca00 --- /dev/null +++ b/src/main/java/org/writer/annotation/CsvWritable.java @@ -0,0 +1,15 @@ +package org.writer.annotation; + + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.FIELD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +public @interface CsvWritable { + boolean value() default true; +} diff --git a/src/main/java/org/writer/model/Person.java b/src/main/java/org/writer/model/Person.java index ba8576b..914fa50 100644 --- a/src/main/java/org/writer/model/Person.java +++ b/src/main/java/org/writer/model/Person.java @@ -3,20 +3,24 @@ import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import org.writer.annotation.CsvWritable; @Data @Builder @AllArgsConstructor public class Person { - + @CsvWritable private String firstName; private String lastName; + @CsvWritable private int dayOfBirth; + @CsvWritable private Months monthOfBirth; + @CsvWritable(value = false) private int yearOfBirth; } diff --git a/src/main/java/org/writer/model/Student.java b/src/main/java/org/writer/model/Student.java index 2b549c4..caf1268 100644 --- a/src/main/java/org/writer/model/Student.java +++ b/src/main/java/org/writer/model/Student.java @@ -5,13 +5,13 @@ import lombok.Data; import java.util.List; +import org.writer.annotation.CsvWritable; @Data @Builder @AllArgsConstructor +@CsvWritable public class Student { - private String name; - private List score; } \ No newline at end of file diff --git a/src/main/java/org/writer/service/CsvWriter.java b/src/main/java/org/writer/service/CsvWriter.java new file mode 100644 index 0000000..d56bf2b --- /dev/null +++ b/src/main/java/org/writer/service/CsvWriter.java @@ -0,0 +1,103 @@ +package org.writer.service; + +import java.io.IOException; +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Field; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import org.writer.Writable; +import org.writer.annotation.CsvWritable; + +public class CsvWriter implements Writable { + + @Override + public void writeToFile(List data, String fileName) { + if (data == null || data.isEmpty()) { + return; + } + + Object obj = data.stream().filter(Objects::nonNull).findFirst().orElse(null); + if (obj == null) { + return; + } + + Class targetClass = obj.getClass(); + + boolean isClassWriteable = + targetClass.isAnnotationPresent(CsvWritable.class) && targetClass.getAnnotation( + CsvWritable.class).value(); + + List writeableFields = Arrays.stream(targetClass.getDeclaredFields()) + .filter(field -> isClassWriteable || field + .isAnnotationPresent(CsvWritable.class) && field.getAnnotation( + CsvWritable.class).value()).peek(AccessibleObject::trySetAccessible).toList(); + + if (writeableFields.isEmpty()) { + return; + } + + String result = + writeableFields.stream().map(Field::getName).collect(Collectors.joining(",", "", "\n")) + + data.stream() + .map(writeableObj -> { + + if (!writeableObj.getClass().equals(obj.getClass())) { + throw new IllegalArgumentException("Object type inconsistent. Expected %s but found %s".formatted(targetClass, obj.getClass().getName()) ); + } + + return writeableFields.stream() + .map(field -> getFormattedValue(field, writeableObj)) + .collect(Collectors.joining(",")); + } + ).collect(Collectors.joining("\n")); + try { + Files.writeString(Path.of(fileName + ".csv"), result); + } catch (IOException e) { + throw new RuntimeException("Failed to write CSV file: " + fileName, e); + } + } + + private String getFormattedValue(Field field, Object object) { + Object fieldVal; + try { + fieldVal = field.get(object); + } catch (IllegalAccessException e) { + throw new RuntimeException("Could not read field: " + field.getName(), + e); + } + + if (fieldVal == null) { + return ""; + } + + String stringVal; + + if (fieldVal instanceof Collection) { + stringVal = ((Collection) fieldVal).stream() + .map(item -> item == null ? "" : item.toString()) + .collect(Collectors.joining(";")); + } else { + stringVal = String.valueOf(fieldVal); + } + + boolean needsQuotes = + stringVal.contains("\n") + || stringVal.contains("\"") + || stringVal.contains(",") + || stringVal.contains("\r"); + + if (needsQuotes) { + stringVal = stringVal.replace("\"", "\"\""); + stringVal = "\"" + stringVal + "\""; + } + + return stringVal; + } + +} + From 5ab0651e6b872bd6bf1b8ea37b47d4e98fabb9b7 Mon Sep 17 00:00:00 2001 From: ysneve Date: Sat, 25 Jul 2026 20:28:34 +0500 Subject: [PATCH 2/3] Add javadoc for CsvWriter --- src/main/java/org/writer/service/CsvWriter.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/main/java/org/writer/service/CsvWriter.java b/src/main/java/org/writer/service/CsvWriter.java index d56bf2b..f169351 100644 --- a/src/main/java/org/writer/service/CsvWriter.java +++ b/src/main/java/org/writer/service/CsvWriter.java @@ -13,8 +13,21 @@ import org.writer.Writable; import org.writer.annotation.CsvWritable; +/** + * {@code CsvWriter} provides implementation of {@code @Writeable} interface + *

+ * CsvWriter provides method to save data in CSV format using {@code CsvWritable} annotation marks + *

+ */ public class CsvWriter implements Writable { + /** + * Saves list of objects in CSV formatted file + * @param data list of values to be saved + * @param fileName path or name of file + * @throws IllegalArgumentException when list contains objects from different classes + * @throws RuntimeException when can not write to file or can not read object field + */ @Override public void writeToFile(List data, String fileName) { if (data == null || data.isEmpty()) { From 23eefb375ca9a8be441c92c815bff811cc3aa0bc Mon Sep 17 00:00:00 2001 From: ysneve Date: Sun, 26 Jul 2026 22:06:23 +0500 Subject: [PATCH 3/3] Add tests and save examples --- src/main/java/org/writer/Main.java | 47 ++++++-- src/main/java/org/writer/model/Company.java | 16 +++ .../java/org/writer/service/CsvWriter.java | 3 +- .../org/writer/service/CsvWriterTest.java | 113 ++++++++++++++++++ 4 files changed, 165 insertions(+), 14 deletions(-) create mode 100644 src/main/java/org/writer/model/Company.java create mode 100644 src/test/java/org/writer/service/CsvWriterTest.java diff --git a/src/main/java/org/writer/Main.java b/src/main/java/org/writer/Main.java index 5a7241f..21b578d 100644 --- a/src/main/java/org/writer/Main.java +++ b/src/main/java/org/writer/Main.java @@ -1,21 +1,42 @@ package org.writer; -import java.util.Arrays; import java.util.List; +import java.util.Random; +import java.util.stream.Stream; +import net.datafaker.Faker; +import org.writer.model.Months; +import org.writer.model.Person; import org.writer.model.Student; import org.writer.service.CsvWriter; public class Main { - public static void main(String[] args) { - System.out.println("Hello world!"); - var writer = new CsvWriter(); - - var listOfObjects = List.of( - new Student("Sergey", Arrays.asList("A", "B")), - new Student("Sergey", null), - new Student("Alexander", List.of("A", "C"))); - writer.writeToFile(listOfObjects, "Test"); - - writer.writeToFile(List.of(1, 2), "test"); - } + + public static void main(String[] args) { + CsvWriter writer = new CsvWriter(); + Faker faker = new Faker(); + int lower = 65; + int upper = 90; + Random random = new Random(); + Months[] months = Months.values(); + + List students = Stream.generate(() -> new Student( + faker.name().firstName(), + Stream.generate(() -> Character.toString((char) random.nextInt(lower, upper))) + .limit(random.nextInt(0, 5)).toList())) + .limit(5).toList(); + + writer.writeToFile(students, "students"); + + List persons = Stream.generate(() -> new Person( + faker.name().firstName(), + faker.name().lastName(), + random.nextInt(1, 31), + months[random.nextInt(months.length)], + faker.timeAndDate().birthday(18, 35).getYear())) + .limit(5).toList(); + + writer.writeToFile(persons, "persons.csv"); + + + } } \ No newline at end of file diff --git a/src/main/java/org/writer/model/Company.java b/src/main/java/org/writer/model/Company.java new file mode 100644 index 0000000..32d6453 --- /dev/null +++ b/src/main/java/org/writer/model/Company.java @@ -0,0 +1,16 @@ +package org.writer.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import org.writer.annotation.CsvWritable; + +@Data +@Builder +@AllArgsConstructor +@CsvWritable +public class Company { + private String name; + private String street; + private String registrationNumber; +} diff --git a/src/main/java/org/writer/service/CsvWriter.java b/src/main/java/org/writer/service/CsvWriter.java index f169351..c574227 100644 --- a/src/main/java/org/writer/service/CsvWriter.java +++ b/src/main/java/org/writer/service/CsvWriter.java @@ -69,7 +69,8 @@ public void writeToFile(List data, String fileName) { } ).collect(Collectors.joining("\n")); try { - Files.writeString(Path.of(fileName + ".csv"), result); + String filePath = fileName.concat(fileName.endsWith(".csv") ? "" : ".csv"); + Files.writeString(Path.of(filePath), result); } catch (IOException e) { throw new RuntimeException("Failed to write CSV file: " + fileName, e); } diff --git a/src/test/java/org/writer/service/CsvWriterTest.java b/src/test/java/org/writer/service/CsvWriterTest.java new file mode 100644 index 0000000..237b47b --- /dev/null +++ b/src/test/java/org/writer/service/CsvWriterTest.java @@ -0,0 +1,113 @@ +package org.writer.service; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; +import java.util.stream.Stream; +import net.datafaker.Faker; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.writer.model.Company; +import org.writer.model.Months; +import org.writer.model.Person; +import org.writer.model.Student; + +class CsvWriterTest { + + private final Faker faker = new Faker(); + private CsvWriter csvWriter; + private Path csvPath; + + @BeforeEach + void setUp(@TempDir Path tempDir) { + this.csvWriter = new CsvWriter(); + this.csvPath = tempDir.resolve("file.csv"); + + } + + @Test + void writeToFile_EmptyFields() throws IOException { + List students = List.of( + new Student(null, List.of("A", "B")), + new Student("Vladimir", null)); + + List companies = List.of( + new Company("123PL Limited", "Field View, Castledon Road, Downham, Billericay, CM11 1LH", null), + new Company("2B&C Logistics Ltd", null, "08229231")); + + csvWriter.writeToFile(students, csvPath.toString()); + + List lines = Files.readAllLines(csvPath); + + assertEquals(3, lines.size()); + assertEquals(",A;B", lines.get(1)); + assertEquals("Vladimir,", lines.get(2)); + + csvWriter.writeToFile(companies, csvPath.toString()); + + lines = Files.readAllLines(csvPath); + + assertEquals(3, lines.size()); + assertEquals("123PL Limited,\"Field View, Castledon Road, Downham, Billericay, CM11 1LH\",", lines.get(1)); + assertEquals("2B&C Logistics Ltd,,08229231", lines.get(2)); + + + } + + @Test + void writeToFile_ValidStructure() throws IOException { + + List students = List.of( + new Student("Alexey", List.of("A", "B")), + new Student("Vladimir", List.of("D", "A", "C", "B")), + new Student("Daniil", Collections.emptyList()), + new Student("Matvey", List.of("E"))); + + csvWriter.writeToFile(students, csvPath.toString()); + + List lines = Files.readAllLines(csvPath); + + assertEquals(5, lines.size()); + assertEquals("Alexey,A;B", lines.get(1)); + assertEquals("Vladimir,D;A;C;B", lines.get(2)); + assertEquals("Daniil,", lines.get(3)); + assertEquals("Matvey,E", lines.get(4)); + + List persons = List.of( + new Person("First", "Last", 12, Months.APRIL, 1999), + new Person("Name", "Test", 29, Months.MARCH, 2000), + new Person("Some", "Person", 28, Months.FEBRUARY, 1956), + new Person("John", "Doe", 1, Months.JANUARY, 1981) + ); + + csvWriter.writeToFile(persons, csvPath.toString()); + + lines = Files.readAllLines(csvPath); + + assertEquals(5, lines.size()); + assertEquals("First,12,APRIL", lines.get(1)); + assertEquals("Name,29,MARCH", lines.get(2)); + assertEquals("Some,28,FEBRUARY", lines.get(3)); + assertEquals("John,1,JANUARY", lines.get(4)); + + List companies = List.of( + new Company("123PL Limited", "Field View, Castledon Road, Downham, Billericay, CM11 1LH", "07682974"), + new Company("2B&C Logistics Ltd", "5 Copperhouse Court, Caldecotte, Milton Keynes, MK7 8NL", "08229231"), + new Company("Benappi Fine Art Limited", "5th Floor 86 Jermyn Street, London, SW1Y 6AW", "08522336") + ); + + csvWriter.writeToFile(companies, csvPath.toString()); + + lines = Files.readAllLines(csvPath); + + assertEquals(4, lines.size()); + assertEquals("123PL Limited,\"Field View, Castledon Road, Downham, Billericay, CM11 1LH\",07682974", lines.get(1)); + assertEquals("2B&C Logistics Ltd,\"5 Copperhouse Court, Caldecotte, Milton Keynes, MK7 8NL\",08229231", lines.get(2)); + assertEquals("Benappi Fine Art Limited,\"5th Floor 86 Jermyn Street, London, SW1Y 6AW\",08522336", lines.get(3)); + } +} \ No newline at end of file