DomainTemplateCsvCodec.java

package fr.tiogars.domaintemplate.domains.domaintemplate.services;

import java.util.ArrayList;
import java.util.List;

import org.springframework.stereotype.Component;

import fr.tiogars.domaintemplate.domains.domaintemplate.entities.DomainTemplate;
import fr.tiogars.domaintemplate.domains.domaintemplate.models.CreateDomainTemplateRequest;

/**
 * Serializes and parses DomainTemplates in the CSV import/export format.
 *
 * @since 0.1.0
 */
@Component
public class DomainTemplateCsvCodec {

    private static final String HEADER = "id,code,label,description,createdDate,lastModifiedDate";

    /**
     * Serializes records to CSV.
     *
     * @param records records to serialize
     * @return CSV document
     */
    public String serialize(List<DomainTemplate> records) {
        List<String> lines = new ArrayList<>();
        lines.add(HEADER);
        for (DomainTemplate template : records) {
            lines.add(String.join(",",
                escape(String.valueOf(template.id())),
                escape(template.code()),
                escape(template.label()),
                escape(template.description() == null ? "" : template.description()),
                escape(template.createdDate().toString()),
                escape(template.lastModifiedDate().toString())));
        }
        return String.join("\n", lines) + "\n";
    }

    /**
     * Parses records from CSV.
     *
     * @param csv CSV document
     * @return parsed records
     */
    public List<CreateDomainTemplateRequest> parse(String csv) {
        List<List<String>> rows = parseRows(csv);
        if (rows.isEmpty()) {
            return List.of();
        }
        if (!HEADER.equals(String.join(",", rows.getFirst()))) {
            throw new IllegalArgumentException("CSV header must be " + HEADER + ".");
        }

        List<CreateDomainTemplateRequest> records = new ArrayList<>();
        for (int index = 1; index < rows.size(); index++) {
            List<String> row = rows.get(index);
            if (row.size() == 1 && row.getFirst().isBlank()) {
                continue;
            }
            if (row.size() != 6) {
                throw new IllegalArgumentException("CSV row " + (index + 1) + " must contain six columns.");
            }
            records.add(parseRecord(row, index + 1));
        }
        return records;
    }

    private CreateDomainTemplateRequest parseRecord(List<String> row, int lineNumber) {
        String code = row.get(1).trim();
        String label = row.get(2).trim();
        if (code.isBlank() || label.isBlank()) {
            throw new IllegalArgumentException("CSV row " + lineNumber + " must contain code and label.");
        }
        String description = row.get(3).isBlank() ? null : row.get(3);
        return new CreateDomainTemplateRequest(code, label, description);
    }

    private static List<List<String>> parseRows(String csv) {
        List<List<String>> rows = new ArrayList<>();
        List<String> row = new ArrayList<>();
        StringBuilder field = new StringBuilder();
        boolean quoted = false;

        for (int index = 0; index < csv.length(); index++) {
            char character = csv.charAt(index);
            if (quoted) {
                if (character == '"') {
                    if (index + 1 < csv.length() && csv.charAt(index + 1) == '"') {
                        field.append('"');
                        index++;
                    } else {
                        quoted = false;
                    }
                } else {
                    field.append(character);
                }
            } else if (character == '"') {
                quoted = true;
            } else if (character == ',') {
                row.add(field.toString());
                field.setLength(0);
            } else if (character == '\n') {
                row.add(field.toString());
                rows.add(row);
                row = new ArrayList<>();
                field.setLength(0);
            } else if (character != '\r') {
                field.append(character);
            }
        }

        if (quoted) {
            throw new IllegalArgumentException("CSV contains an unterminated quoted field.");
        }
        if (!field.isEmpty() || !row.isEmpty()) {
            row.add(field.toString());
            rows.add(row);
        }
        return rows;
    }

    private static String escape(String value) {
        if (value.contains(",") || value.contains("\n") || value.contains("\r") || value.contains("\"")) {
            return "\"" + value.replace("\"", "\"\"") + "\"";
        }
        return value;
    }
}