diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..63b3a9a
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,27 @@
+plugins {
+ id 'java-library'
+}
+
+group = 'org.writer'
+version = '1.0-SNAPSHOT'
+
+java {
+ toolchain {
+ languageVersion = JavaLanguageVersion.of(17)
+ }
+}
+
+repositories {
+ mavenCentral()
+}
+
+dependencies {
+ compileOnly 'org.projectlombok:lombok:1.18.34'
+ annotationProcessor 'org.projectlombok:lombok:1.18.34'
+
+ testImplementation 'org.junit.jupiter:junit-jupiter:5.8.1'
+}
+
+test {
+ useJUnitPlatform()
+}
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
deleted file mode 100644
index effc1bd..0000000
--- a/pom.xml
+++ /dev/null
@@ -1,32 +0,0 @@
-
-
Allows overriding column name, order, and delimiter for collections.
+ */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface CsvColumn { + + String name() default ""; + + int order() default Integer.MAX_VALUE; + + String collectionDelimiter() default ";"; +} diff --git a/src/main/java/org/writer/csv/CsvFile.java b/src/main/java/org/writer/csv/CsvFile.java new file mode 100644 index 0000000..497bad1 --- /dev/null +++ b/src/main/java/org/writer/csv/CsvFile.java @@ -0,0 +1,18 @@ +package org.writer.csv; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Defines global CSV configuration for a class. + *Allows setting the delimiter between columns.
+ */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface CsvFile { + + String columnDelimiter() default ","; + +} diff --git a/src/main/java/org/writer/csv/CsvIgnore.java b/src/main/java/org/writer/csv/CsvIgnore.java new file mode 100644 index 0000000..2665c41 --- /dev/null +++ b/src/main/java/org/writer/csv/CsvIgnore.java @@ -0,0 +1,14 @@ +package org.writer.csv; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Excludes a field from CSV serialization. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface CsvIgnore { +} diff --git a/src/main/java/org/writer/exception/CsvException.java b/src/main/java/org/writer/exception/CsvException.java new file mode 100644 index 0000000..070ff56 --- /dev/null +++ b/src/main/java/org/writer/exception/CsvException.java @@ -0,0 +1,7 @@ +package org.writer.exception; + +public class CsvException extends RuntimeException { + public CsvException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/org/writer/exception/CsvFieldAccessException.java b/src/main/java/org/writer/exception/CsvFieldAccessException.java new file mode 100644 index 0000000..98f8799 --- /dev/null +++ b/src/main/java/org/writer/exception/CsvFieldAccessException.java @@ -0,0 +1,7 @@ +package org.writer.exception; + +public class CsvFieldAccessException extends CsvException { + public CsvFieldAccessException(String fieldName, Throwable cause) { + super("Field access error: " + fieldName, cause); + } +} diff --git a/src/main/java/org/writer/exception/CsvWriteException.java b/src/main/java/org/writer/exception/CsvWriteException.java new file mode 100644 index 0000000..c308bb3 --- /dev/null +++ b/src/main/java/org/writer/exception/CsvWriteException.java @@ -0,0 +1,7 @@ +package org.writer.exception; + +public class CsvWriteException extends CsvException { + public CsvWriteException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/org/writer/Writable.java b/src/main/java/org/writer/writer/Writable.java similarity index 80% rename from src/main/java/org/writer/Writable.java rename to src/main/java/org/writer/writer/Writable.java index 9f60c45..0f87ba1 100644 --- a/src/main/java/org/writer/Writable.java +++ b/src/main/java/org/writer/writer/Writable.java @@ -1,4 +1,4 @@ -package org.writer; +package org.writer.writer; import java.util.List; diff --git a/src/main/java/org/writer/writer/csv/CsvWriter.java b/src/main/java/org/writer/writer/csv/CsvWriter.java new file mode 100644 index 0000000..0713681 --- /dev/null +++ b/src/main/java/org/writer/writer/csv/CsvWriter.java @@ -0,0 +1,215 @@ +package org.writer.writer.csv; + +import org.writer.csv.CsvColumn; +import org.writer.csv.CsvFile; +import org.writer.csv.CsvIgnore; +import org.writer.exception.CsvFieldAccessException; +import org.writer.exception.CsvWriteException; +import org.writer.writer.Writable; + +import java.io.FileWriter; +import java.io.IOException; +import java.lang.reflect.Field; +import java.util.*; +import java.util.logging.Logger; +import java.util.stream.Collectors; + + +/** + * Utility class for writing annotated Java objects into CSV files. + *Supports field-level configuration via {@link CsvColumn} and class-level + * configuration via {@link CsvFile}. Fields annotated with {@link CsvIgnore} + * are excluded from output.
+ * + *Collections and maps are serialized using custom delimiters. Primitive + * types and enums are converted via {@link Object#toString()}.
+ * + *Throws {@link org.writer.exception.CsvWriteException} if writing fails, + * and {@link org.writer.exception.CsvFieldAccessException} if field access + * is not possible.
+ */ +public class CsvWriter implements Writable { + + private static final Logger logger = Logger.getLogger(CsvWriter.class.getName()); + + /** + * Writes a list of objects into a CSV file. + *
+ * The header row is generated from field names or {@link CsvColumn#name()}.
+ * Each object is serialized into a row according to field order.
+ *
+ * @param data list of objects to serialize
+ * @param fileName target file path
+ * @throws CsvWriteException if writing fails
+ */
+ @Override
+ public void writeToFile(List> data, String fileName) {
+ if (!validateInput(data)) return;
+
+ Class> clazz = data.get(0).getClass();
+ List
+ * Excludes fields annotated with {@link CsvIgnore} and sorts them
+ * according to {@link CsvColumn#order()}.
+ *
+ * @param clazz object class
+ * @return sorted list of accessible fields
+ */
+ private List
+ * Uses {@link CsvColumn#name()} if present, otherwise defaults to field name.
+ *
+ * @param field field to resolve
+ * @return column name
+ */
+ private String getColumnName(Field field) {
+ CsvColumn column = field.getAnnotation(CsvColumn.class);
+ return (column != null && !column.name().isEmpty()) ? column.name() : field.getName();
+ }
+
+ /**
+ * Retrieves string value of a field from an object.
+ *
+ * Supports collections and maps with custom delimiters.
+ *
+ * @param obj object containing the field
+ * @param field field to access
+ * @return string representation of field value
+ * @throws CsvFieldAccessException if field access fails
+ */
+ private String getFieldValueAsString(Object obj, Field field) {
+ try {
+ Object value = field.get(obj);
+ if (value instanceof Collection) {
+ CsvColumn column = field.getAnnotation(CsvColumn.class);
+ String delimiter = column != null ? column.collectionDelimiter() : getDefaultCollectionDelimiter();
+ return serialize((Collection>) value, delimiter);
+ }
+
+ if (value instanceof Map) {
+ CsvColumn column = field.getAnnotation(CsvColumn.class);
+ String delimiter = column != null ? column.collectionDelimiter() : getDefaultCollectionDelimiter();
+ return serialize((Map, ?>) value, delimiter);
+ }
+ return value != null ? value.toString() : "";
+ } catch (IllegalAccessException e) {
+ logger.severe("Error access to field: " + field.getName());
+ throw new CsvFieldAccessException(field.getName(), e);
+ }
+ }
+
+ /**
+ * Returns default collection delimiter from {@link CsvColumn}.
+ */
+ private static String getDefaultCollectionDelimiter() {
+ return (String) Arrays.stream(CsvColumn.class.getDeclaredMethods())
+ .filter(m -> m.getName().equals("collectionDelimiter"))
+ .findFirst().get() // мы уверены, что метод существует, но, возможно, лучше воспользоваться orElseThrow() для самодостаточности метода
+ .getDefaultValue();
+ }
+
+ /**
+ * Returns default column delimiter from {@link CsvFile}.
+ */
+ private static String getDefaultColumnDelimiter() {
+ return (String) Arrays.stream(CsvFile.class.getDeclaredMethods())
+ .filter(m -> m.getName().equals("columnDelimiter"))
+ .findFirst().get() // мы уверены, что метод существует, но, возможно, лучше воспользоваться orElseThrow() для самодостаточности метода
+ .getDefaultValue();
+ }
+
+ /**
+ * Converts a {@link Collection} into a string joined by the given delimiter.
+ * Null elements are represented as empty strings.
+ */
+ private String serialize(Collection> collection, String delimiter) {
+ return collection.stream()
+ .map(e -> e == null ? "" : e.toString())
+ .collect(Collectors.joining(delimiter));
+ }
+
+ /**
+ * Converts a {@link Map} into a string of key=value pairs joined by the given delimiter.
+ * Null keys or values are represented as empty strings.
+ */
+ private String serialize(Map, ?> map, String delimiter) {
+ return map.entrySet().stream()
+ .map(e -> new StringBuilder().append(e.getKey() == null
+ ? ""
+ : e.getKey()).append("=").append(e.getValue() == null ? "" : e.getValue()).toString())
+ .collect(Collectors.joining(delimiter));
+ }
+}
diff --git a/src/test/java/org/writter/CsvWriterTest.java b/src/test/java/org/writter/CsvWriterTest.java
new file mode 100644
index 0000000..9260850
--- /dev/null
+++ b/src/test/java/org/writter/CsvWriterTest.java
@@ -0,0 +1,65 @@
+package org.writter;
+
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.writer.model.Months;
+import org.writer.model.Person;
+import org.writer.writer.csv.CsvWriter;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class CsvWriterTest {
+
+ @TempDir
+ Path tempDir;
+
+ @Test
+ void testWriteValidData() throws IOException {
+ CsvWriter writer = new CsvWriter();
+ List