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
11 changes: 8 additions & 3 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,21 @@
<version>1.0-SNAPSHOT</version>

<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
<dependency>
<groupId>net.datafaker</groupId>
<artifactId>datafaker</artifactId>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.34</version>
<version>1.18.36</version>
<scope>provided</scope>
</dependency>
<dependency>
Expand Down
41 changes: 38 additions & 3 deletions src/main/java/org/writer/Main.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,42 @@
package org.writer;

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!");
}

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<Student> 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<Person> 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");


}
}
15 changes: 15 additions & 0 deletions src/main/java/org/writer/annotation/CsvWritable.java
Original file line number Diff line number Diff line change
@@ -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;
}
16 changes: 16 additions & 0 deletions src/main/java/org/writer/model/Company.java
Original file line number Diff line number Diff line change
@@ -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;
}
6 changes: 5 additions & 1 deletion src/main/java/org/writer/model/Person.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

}
4 changes: 2 additions & 2 deletions src/main/java/org/writer/model/Student.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> score;
}
117 changes: 117 additions & 0 deletions src/main/java/org/writer/service/CsvWriter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
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;

/**
* {@code CsvWriter} provides implementation of {@code @Writeable} interface
* <p>
* CsvWriter provides method to save data in CSV format using {@code CsvWritable} annotation marks
* </p>
*/
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()) {
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<Field> 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 {
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);
}
}

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;
}

}

113 changes: 113 additions & 0 deletions src/test/java/org/writer/service/CsvWriterTest.java
Original file line number Diff line number Diff line change
@@ -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<Student> students = List.of(
new Student(null, List.of("A", "B")),
new Student("Vladimir", null));

List<Company> 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<String> 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<Student> 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<String> 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<Person> 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<Company> 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));
}
}