1 package fr.tiogars.inception.domains.domaintemplate.services;
2
3 import java.util.ArrayList;
4 import java.util.List;
5
6 import org.springframework.stereotype.Component;
7
8 import fr.tiogars.inception.domains.domaintemplate.models.CreateDomainTemplateRequest;
9 import fr.tiogars.inception.domains.domaintemplate.models.DomainTemplate;
10
11
12
13
14
15
16 @Component
17 public class DomainTemplateCsvCodec {
18
19 private static final String HEADER = "id,code,label,description,createdDate,lastModifiedDate";
20
21
22
23
24
25
26
27 public String serialize(List<DomainTemplate> records) {
28 List<String> lines = new ArrayList<>();
29 lines.add(HEADER);
30 for (DomainTemplate template : records) {
31 lines.add(String.join(",",
32 escape(String.valueOf(template.id())),
33 escape(template.code()),
34 escape(template.label()),
35 escape(template.description() == null ? "" : template.description()),
36 escape(template.createdDate().toString()),
37 escape(template.lastModifiedDate().toString())));
38 }
39 return String.join("\n", lines) + "\n";
40 }
41
42
43
44
45
46
47
48 public List<CreateDomainTemplateRequest> parse(String csv) {
49 List<List<String>> rows = parseRows(csv);
50 if (rows.isEmpty()) {
51 return List.of();
52 }
53 if (!HEADER.equals(String.join(",", rows.getFirst()))) {
54 throw new IllegalArgumentException("CSV header must be " + HEADER + ".");
55 }
56
57 List<CreateDomainTemplateRequest> records = new ArrayList<>();
58 for (int index = 1; index < rows.size(); index++) {
59 List<String> row = rows.get(index);
60 if (row.size() == 1 && row.getFirst().isBlank()) {
61 continue;
62 }
63 if (row.size() != 6) {
64 throw new IllegalArgumentException("CSV row " + (index + 1) + " must contain six columns.");
65 }
66 records.add(parseRecord(row, index + 1));
67 }
68 return records;
69 }
70
71 private CreateDomainTemplateRequest parseRecord(List<String> row, int lineNumber) {
72 String code = row.get(1).trim();
73 String label = row.get(2).trim();
74 if (code.isBlank() || label.isBlank()) {
75 throw new IllegalArgumentException("CSV row " + lineNumber + " must contain code and label.");
76 }
77 String description = row.get(3).isBlank() ? null : row.get(3);
78 return new CreateDomainTemplateRequest(code, label, description);
79 }
80
81 private static List<List<String>> parseRows(String csv) {
82 List<List<String>> rows = new ArrayList<>();
83 List<String> row = new ArrayList<>();
84 StringBuilder field = new StringBuilder();
85 boolean quoted = false;
86
87 for (int index = 0; index < csv.length(); index++) {
88 char character = csv.charAt(index);
89 if (quoted) {
90 if (character == '"') {
91 if (index + 1 < csv.length() && csv.charAt(index + 1) == '"') {
92 field.append('"');
93 index++;
94 } else {
95 quoted = false;
96 }
97 } else {
98 field.append(character);
99 }
100 } else if (character == '"') {
101 quoted = true;
102 } else if (character == ',') {
103 row.add(field.toString());
104 field.setLength(0);
105 } else if (character == '\n') {
106 row.add(field.toString());
107 rows.add(row);
108 row = new ArrayList<>();
109 field.setLength(0);
110 } else if (character != '\r') {
111 field.append(character);
112 }
113 }
114
115 if (quoted) {
116 throw new IllegalArgumentException("CSV contains an unterminated quoted field.");
117 }
118 if (!field.isEmpty() || !row.isEmpty()) {
119 row.add(field.toString());
120 rows.add(row);
121 }
122 return rows;
123 }
124
125 private static String escape(String value) {
126 if (value.contains(",") || value.contains("\n") || value.contains("\r") || value.contains("\"")) {
127 return "\"" + value.replace("\"", "\"\"") + "\"";
128 }
129 return value;
130 }
131 }