-
Notifications
You must be signed in to change notification settings - Fork 23
Expand file tree
/
Copy pathTabularArrayDecoder.java
More file actions
244 lines (212 loc) · 9.5 KB
/
Copy pathTabularArrayDecoder.java
File metadata and controls
244 lines (212 loc) · 9.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
package dev.toonformat.jtoon.decoder;
import dev.toonformat.jtoon.util.StringEscaper;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import static dev.toonformat.jtoon.util.Headers.TABULAR_HEADER_PATTERN;
/**
* Handles decoding of tabular arrays to JSON format.
*/
public class TabularArrayDecoder {
private TabularArrayDecoder() {
throw new UnsupportedOperationException("Utility class cannot be instantiated");
}
/**
* Parses tabular array format where each row contains delimiter-separated
* values.
* Example: items[2]{id,name}:\n 1,Ada\n 2,Bob
* @param header the string representation of header
* @param depth depth of array
* @param arrayDelimiter the type of delimiter used in the array
* @param context decode object in order to deal with lines, delimiter and options
* @return tabular array converted to JSON format
*/
public static List<Object> parseTabularArray(String header, int depth, String arrayDelimiter, DecodeContext context) {
Matcher matcher = TABULAR_HEADER_PATTERN.matcher(header);
if (!matcher.find()) {
return new ArrayList<>();
}
String keysStr = matcher.group(4);
List<String> keys = parseTabularKeys(keysStr, arrayDelimiter, context);
List<Object> result = new ArrayList<>();
context.currentLine++;
// Determine the expected row depth dynamically from the first non-blank line
int expectedRowDepth;
if (context.currentLine < context.lines.length) {
int nextNonBlankLine = DecodeHelper.findNextNonBlankLine(context.currentLine, context);
if (nextNonBlankLine < context.lines.length) {
expectedRowDepth = DecodeHelper.getDepth(context.lines[nextNonBlankLine], context);
} else {
expectedRowDepth = depth + 1;
}
} else {
expectedRowDepth = depth + 1;
}
while (context.currentLine < context.lines.length) {
if (!processTabularArrayLine(expectedRowDepth, keys, arrayDelimiter, result, context)) {
break;
}
}
ArrayDecoder.validateArrayLength(header, result.size());
return result;
}
/**
* Parses tabular header keys from field specification.
* Validates delimiter consistency between bracket and brace fields.
*/
private static List<String> parseTabularKeys(String keysStr, String arrayDelimiter, DecodeContext context) {
// Validate delimiter mismatch between bracket and brace fields
if (context.options.strict()) {
validateKeysDelimiter(keysStr, arrayDelimiter);
}
List<String> result = new ArrayList<>();
List<String> rawValues = ArrayDecoder.parseDelimitedValues(keysStr, arrayDelimiter);
for (String key : rawValues) {
result.add(StringEscaper.unescape(key));
}
return result;
}
/**
* Validates delimiter consistency in tabular header keys.
*/
private static void validateKeysDelimiter(String keysStr, String expectedDelimiter) {
char expectedChar = expectedDelimiter.charAt(0);
boolean inQuotes = false;
boolean escaped = false;
for (int i = 0; i < keysStr.length(); i++) {
char c = keysStr.charAt(i);
if (escaped) {
escaped = false;
} else if (c == '\\') {
escaped = true;
} else if (c == '"') {
inQuotes = !inQuotes;
} else if (!inQuotes) {
checkDelimiterMismatch(expectedChar, c);
}
}
}
/**
* Checks for delimiter mismatch and throws an exception if found.
*/
private static void checkDelimiterMismatch(char expectedChar, char actualChar) {
if (expectedChar == '\t' && actualChar == ',') {
throw new IllegalArgumentException(
"Delimiter mismatch: bracket declares tab, brace fields use comma");
}
if (expectedChar == '|' && actualChar == ',') {
throw new IllegalArgumentException(
"Delimiter mismatch: bracket declares pipe, brace fields use comma");
}
if (expectedChar == ',' && (actualChar == '\t' || actualChar == '|')) {
throw new IllegalArgumentException(
"Delimiter mismatch: bracket declares comma, brace fields use different delimiter");
}
}
/**
* Processes a single line in a tabular array.
* Returns true if parsing should continue, false if an array should terminate.
*/
private static boolean processTabularArrayLine(int expectedRowDepth, List<String> keys, String arrayDelimiter,
List<Object> result, DecodeContext context) {
String line = context.lines[context.currentLine];
if (DecodeHelper.isBlankLine(line)) {
return !handleBlankLineInTabularArray(expectedRowDepth, context);
}
int lineDepth = DecodeHelper.getDepth(line, context);
if (shouldTerminateTabularArray(line, lineDepth, expectedRowDepth, context)) {
return false;
}
if (processTabularRow(line, lineDepth, expectedRowDepth, keys, arrayDelimiter, result, context)) {
context.currentLine++;
}
return true;
}
/**
* Handles blank line processing in a tabular array.
* Returns true if an array should terminate, false if a line should be skipped.
*/
private static boolean handleBlankLineInTabularArray(int expectedRowDepth, DecodeContext context) {
int nextNonBlankLine = DecodeHelper.findNextNonBlankLine(context.currentLine + 1, context);
if (nextNonBlankLine < context.lines.length) {
int nextDepth = DecodeHelper.getDepth(context.lines[nextNonBlankLine], context);
// Header depth is one level above the expected row depth
int headerDepth = expectedRowDepth - 1;
if (nextDepth <= headerDepth) {
return true;
}
}
// Blank line is inside the array
if (context.options.strict()) {
throw new IllegalArgumentException(
"Blank line inside tabular array at line " + (context.currentLine + 1));
}
// In non-strict mode, skip blank lines
context.currentLine++;
return false;
}
/**
* Determines if tabular array parsing should terminate based online depth.
* Returns true if array should terminate, false otherwise.
*/
private static boolean shouldTerminateTabularArray(String line, int lineDepth, int expectedRowDepth, DecodeContext context) {
// Header depth is one level above expected row depth
int headerDepth = expectedRowDepth - 1;
if (lineDepth < expectedRowDepth) {
if (lineDepth == headerDepth) {
String content = line.substring(headerDepth * context.options.indent());
int colonIdx = DecodeHelper.findUnquotedColon(content);
if (colonIdx > 0) {
return true; // Key-value pair at same depth - terminate array
}
}
return true; // Line depth is less than expected - terminate
}
// Check for key-value pair at expected row depth
if (lineDepth == expectedRowDepth) {
String rowContent = line.substring(expectedRowDepth * context.options.indent());
int colonIdx = DecodeHelper.findUnquotedColon(rowContent);
return colonIdx > 0; // Key-value pair at same depth as rows - terminate array
}
return false;
}
/**
* Processes a tabular row if it matches the expected depth.
* Returns true if line was processed and currentLine should be incremented,
* false otherwise.
*/
private static boolean processTabularRow(String line, int lineDepth, int expectedRowDepth, List<String> keys,
String arrayDelimiter, List<Object> result, DecodeContext context) {
if (lineDepth == expectedRowDepth) {
String rowContent = line.substring(expectedRowDepth * context.options.indent());
Map<String, Object> row = parseTabularRow(rowContent, keys, arrayDelimiter, context);
result.add(row);
return true;
} else if (lineDepth > expectedRowDepth) {
// Line is deeper than expected - might be nested content, skip it
context.currentLine++;
return false;
}
return true;
}
/**
* Parses a tabular row into a Map using the provided keys.
* Validates that the row uses the correct delimiter.
*/
private static Map<String, Object> parseTabularRow(String rowContent, List<String> keys, String arrayDelimiter, DecodeContext context) {
Map<String, Object> row = new LinkedHashMap<>();
List<Object> values = ArrayDecoder.parseArrayValues(rowContent, arrayDelimiter);
// Validate value count matches key count
if (context.options.strict() && values.size() != keys.size()) {
throw new IllegalArgumentException(
String.format("Tabular row value count (%d) does not match header field count (%d)",
values.size(), keys.size()));
}
for (int i = 0; i < keys.size() && i < values.size(); i++) {
row.put(keys.get(i), values.get(i));
}
return row;
}
}