This is an automated email from the ASF dual-hosted git repository.
jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git
The following commit(s) were added to refs/heads/master by this push:
new b5fece3386 CSV/YAML support
b5fece3386 is described below
commit b5fece338643acc9035c68ac51a249dc76817a0c
Author: James Bognar <[email protected]>
AuthorDate: Sun Mar 1 07:32:07 2026 -0500
CSV/YAML support
---
.../main/java/org/apache/juneau/csv/CsvParser.java | 33 ++-
.../org/apache/juneau/csv/CsvParserSession.java | 210 ++++++++++++++-
.../main/java/org/apache/juneau/csv/CsvReader.java | 222 ++++++++++++++++
.../java/org/apache/juneau/csv/CsvSerializer.java | 27 +-
.../apache/juneau/csv/CsvSerializerSession.java | 64 +++--
.../main/java/org/apache/juneau/csv/CsvWriter.java | 30 ++-
.../java/org/apache/juneau/yaml/YamlParser.java | 31 +++
.../org/apache/juneau/yaml/YamlSerializer.java | 16 ++
.../test/java/org/apache/juneau/ComboInput.java | 18 +-
.../org/apache/juneau/ComboRoundTripTest_Base.java | 22 ++
.../org/apache/juneau/ComboRoundTrip_Tester.java | 6 +-
.../org/apache/juneau/ComboSerializeTest_Base.java | 40 +++
.../org/apache/juneau/ComboSerialize_Tester.java | 12 +-
.../a/rttests/RoundTripAddClassAttrs_Test.java | 11 +
.../juneau/a/rttests/RoundTripBeanMaps_Test.java | 11 +
.../a/rttests/RoundTripBeansWithBuilders_Test.java | 11 +
.../juneau/a/rttests/RoundTripDateTime_Test.java | 6 +
.../a/rttests/RoundTripLargeObjects_Test.java | 11 +
.../juneau/a/rttests/RoundTripMaps_Test.java | 12 +
.../juneau/a/rttests/RoundTripTest_Base.java | 85 ++++++
.../a/rttests/RoundTripTransformBeans_Test.java | 11 +
.../apache/juneau/a/rttests/RoundTrip_Tester.java | 42 ++-
.../java/org/apache/juneau/csv/CsvParser_Test.java | 292 +++++++++++++++++++++
.../test/java/org/apache/juneau/csv/Csv_Test.java | 80 ++++++
.../org/apache/juneau/marshaller/Csv_Test.java | 31 ++-
25 files changed, 1277 insertions(+), 57 deletions(-)
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvParser.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvParser.java
index c04bc47081..6d84768d0b 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvParser.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvParser.java
@@ -30,7 +30,38 @@ import org.apache.juneau.commons.reflect.*;
import org.apache.juneau.parser.*;
/**
- * TODO - Work in progress. CSV parser.
+ * Parses CSV (Comma Separated Values) input into Java objects.
+ *
+ * <p>
+ * Parses RFC 4180-compliant CSV into collections of beans, maps, or simple
values. Each row becomes an
+ * element; the header row defines column names.
+ *
+ * <h5 class='section'>Data Structures Incompatible with CSV (vs. JSON):</h5>
+ * <p>
+ * CSV is a flat, tabular format. Unlike {@link
org.apache.juneau.json.JsonParser JSON}, which
+ * parses nested structures and supports type discriminators, CSV cannot
faithfully parse:
+ * <ul>
+ * <li><b>Raw primitive arrays</b> ({@code byte[]}, {@code int[]}, etc.) —
Cells are strings; no
+ * unambiguous encoding. <i>JSON</i> parses arrays and base64-encoded
bytes.
+ * <li><b>Nested beans</b> — Cannot reconstruct bean- or map-valued
properties from flat columns.
+ * <i>JSON</i> parses nested objects directly.
+ * <li><b>Collections/arrays within beans</b> — Cannot parse {@code
List<X>},
+ * {@code Map<K,V>}, or array properties from a single cell.
<i>JSON</i> parses arrays
+ * and nested objects.
+ * <li><b>Generic type parameters</b> — No type discriminator. <i>JSON</i>
uses {@code @type} when
+ * configured with {@code addBeanTypes().addRootType()}.
+ * <li><b>Parent/inherited properties</b> — Bean hierarchy cannot be
reconstructed.
+ * <i>JSON</i> reconstructs flattened properties into the correct bean
hierarchy.
+ * <li><b>Optional wrappers</b> — Round-trip may differ from tree formats.
<i>JSON</i> handles
+ * Optional consistently.
+ * <li><b>Interface/abstract types</b> — No discriminator to select
implementation. <i>JSON</i>
+ * uses {@code @type} with {@code implClass} mappings.
+ * </ul>
+ *
+ * <h5 class='section'>Best Supported:</h5>
+ * <p>
+ * Parsing into {@link java.util.Collection} of flat beans or maps, or single
beans/maps with simple
+ * property types (primitives, strings, numbers, enums, dates).
*
* <h5 class='section'>Notes:</h5><ul>
* <li class='note'>This class is thread safe and reusable.
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvParserSession.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvParserSession.java
index 22d19171bb..794a4296ad 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvParserSession.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvParserSession.java
@@ -16,24 +16,47 @@
*/
package org.apache.juneau.csv;
+import static org.apache.juneau.commons.utils.CollectionUtils.*;
+
import java.io.*;
import java.lang.reflect.*;
import java.nio.charset.*;
import java.util.*;
import java.util.function.*;
+import java.util.Optional;
import org.apache.juneau.*;
+import org.apache.juneau.collections.*;
import org.apache.juneau.httppart.*;
import org.apache.juneau.parser.*;
+import org.apache.juneau.swap.*;
/**
* Session object that lives for the duration of a single use of {@link
CsvParser}.
*
+ * <p>
+ * Parses CSV (Comma Separated Values) input into Java objects. The first row
of the CSV
+ * is treated as a header row providing column names. Subsequent rows are
treated as data rows.
+ *
+ * <p>
+ * The following target type mappings are supported:
+ * <ul>
+ * <li>{@code Collection<Bean>} / {@code Bean[]} — Header row provides
property names; each data row becomes a bean.
+ * <li>{@code Collection<Map>} / {@code Map[]} — Header row provides map
keys; each data row becomes a map.
+ * <li>{@code Collection<SimpleType>} / {@code SimpleType[]} — Single {@code
value} column; each row's value is coerced to the element type.
+ * <li>Single {@code Bean} — Header row + one data row → one bean.
+ * <li>Single {@code Map} — Header row + one data row → one map.
+ * <li>{@code Object} — Returns a {@link JsonList} of {@link JsonMap}
entries.
+ * </ul>
+ *
* <h5 class='section'>Notes:</h5><ul>
* <li class='warn'>This class is not thread safe and is typically
discarded after one use.
* </ul>
- *
*/
+@SuppressWarnings({
+ "unchecked",
+ "rawtypes",
+})
public class CsvParserSession extends ReaderParserSession {
/**
@@ -171,19 +194,186 @@ public class CsvParserSession extends
ReaderParserSession {
super(builder);
}
- @SuppressWarnings({
- "java:S1172" // Future code - parameters reserved for future
implementation
- })
- private static <T> T parseAnything(ClassMeta<T> eType, ParserReader r,
Object outer, BeanPropertyMeta pMeta) throws ParseException {
- throw new ParseException("Not implemented.");
- }
-
@Override /* Overridden from ParserSession */
protected <T> T doParse(ParserPipe pipe, ClassMeta<T> type) throws
IOException, ParseException {
- try (var r = pipe.getParserReader()) {
+ try (var r = CsvReader.from(pipe, ',', '"', isTrimStrings())) {
if (r == null)
return null;
return parseAnything(type, r, getOuter(), null);
}
}
-}
\ No newline at end of file
+
+ /**
+ * Core parse dispatch.
+ *
+ * <p>
+ * Reads the header row, then dispatches to the appropriate parsing
strategy based on the
+ * target type.
+ */
+ private <T> T parseAnything(ClassMeta<T> eType, CsvReader r, Object
outer, BeanPropertyMeta pMeta) throws IOException, ParseException {
+ if (eType == null)
+ eType = (ClassMeta<T>) object();
+
+ var swap = (ObjectSwap<T,Object>) eType.getSwap(this);
+ var builder = (BuilderSwap<T,Object>)
eType.getBuilderSwap(this);
+ ClassMeta<?> sType;
+ if (builder != null)
+ sType = builder.getBuilderClassMeta(this);
+ else if (swap != null)
+ sType = swap.getSwapClassMeta(this);
+ else
+ sType = eType;
+
+ if (sType.isOptional())
+ return (T)
Optional.ofNullable(parseAnything(eType.getElementType(), r, outer, pMeta));
+
+ // Read header row
+ var headers = r.readRow();
+ if (headers == null || headers.isEmpty())
+ return null;
+
+ Object o = null;
+
+ if (sType.isArray()) {
+ var elementType = sType.getElementType();
+ var list = list();
+ for (var row = r.readRow(); row != null; row =
r.readRow())
+ list.add(parseRow(headers, row, elementType,
list));
+ o = toArray(sType, list);
+
+ } else if (sType.isCollection()) {
+ var elementType = sType.getElementType();
+ Collection<Object> l =
sType.canCreateNewInstance(outer) ? (Collection<Object>) sType.newInstance() :
new ArrayList<>();
+ for (var row = r.readRow(); row != null; row =
r.readRow())
+ l.add(parseRow(headers, row, elementType, l));
+ o = l;
+
+ } else if (sType.isBean()) {
+ var row = r.readRow();
+ if (row != null)
+ o = parseRowIntoBean(headers, row, sType,
outer);
+
+ } else if (sType.isMap()) {
+ var row = r.readRow();
+ if (row != null)
+ o = parseRowIntoMap(headers, row, sType, outer);
+
+ } else if (sType.isObject()) {
+ // For Object target type: return a JsonList of
JsonMaps (or a single JsonMap if one row)
+ var results = new JsonList(this);
+ for (var row = r.readRow(); row != null; row =
r.readRow()) {
+ var m = new JsonMap(this);
+ for (var i = 0; i < headers.size(); i++) {
+ var val = i < row.size() ? row.get(i) :
null;
+ m.put(headers.get(i),
parseCellValue(val, object()));
+ }
+ results.add(m);
+ }
+ o = results.isEmpty() ? null : (results.size() == 1 ?
results.get(0) : results);
+ } else {
+ // For simple target types (String, Number, Boolean,
etc.) that are not beans/maps/collections,
+ // treat CSV as a single "value" column. Read the
first data row's value column.
+ var valueColIdx = headers.indexOf("value");
+ if (valueColIdx < 0) valueColIdx = 0;
+ var row = r.readRow();
+ if (row != null && valueColIdx < row.size())
+ o = parseCellValue(row.get(valueColIdx), sType);
+ }
+
+ if (builder != null && o != null)
+ o = builder.build(this, o, eType);
+
+ if (swap != null && o != null)
+ o = unswap(swap, o, eType);
+
+ return (T) o;
+ }
+
+ /**
+ * Parses a single data row into the appropriate element object.
+ */
+ private Object parseRow(List<String> headers, List<String> row,
ClassMeta<?> eType, Object outer) throws ParseException {
+ if (eType == null || eType.isObject()) {
+ var m = new JsonMap(this);
+ for (var i = 0; i < headers.size(); i++) {
+ var val = i < row.size() ? row.get(i) : null;
+ m.put(headers.get(i), parseCellValue(val,
object()));
+ }
+ return m;
+ } else if (eType.isBean()) {
+ return parseRowIntoBean(headers, row, eType, outer);
+ } else if (eType.isMap()) {
+ return parseRowIntoMap(headers, row, eType, outer);
+ } else {
+ // Simple type: use the "value" column (first column)
or the only column present
+ var val = row.isEmpty() ? null : row.get(0);
+ return parseCellValue(val, eType);
+ }
+ }
+
+ /**
+ * Parses a single data row into a bean of the specified type.
+ */
+ private <T> T parseRowIntoBean(List<String> headers, List<String> row,
ClassMeta<T> eType, Object outer) throws ParseException {
+ var m = newBeanMap(outer, eType.inner());
+ for (var i = 0; i < headers.size(); i++) {
+ var header = headers.get(i);
+ var val = i < row.size() ? row.get(i) : null;
+ var pm = m.getPropertyMeta(header);
+ if (pm != null) {
+ setCurrentProperty(pm);
+ var converted = parseCellValue(val,
pm.getClassMeta());
+ pm.set(m, header, converted);
+ setCurrentProperty(null);
+ } else {
+ onUnknownProperty(header, m, val);
+ }
+ }
+ return m.getBean();
+ }
+
+ /**
+ * Parses a single data row into a map of the specified type.
+ */
+ @SuppressWarnings("java:S3740")
+ private Object parseRowIntoMap(List<String> headers, List<String> row,
ClassMeta<?> eType, Object outer) throws ParseException {
+ Map m;
+ if (eType.canCreateNewInstance(outer))
+ m = (Map) eType.newInstance(outer);
+ else
+ m = new JsonMap(this);
+ var keyType = eType.getKeyType() != null ? eType.getKeyType() :
string();
+ var valueType = eType.getValueType() != null ?
eType.getValueType() : object();
+ for (var i = 0; i < headers.size(); i++) {
+ var header = headers.get(i);
+ var val = i < row.size() ? row.get(i) : null;
+ var key = convertAttrToType(m, header, keyType);
+ var value = parseCellValue(val, valueType);
+ m.put(key, value);
+ }
+ return m;
+ }
+
+ /**
+ * Converts a raw CSV cell string value to the target type.
+ *
+ * <p>
+ * The unquoted literal {@code null} maps to Java {@code null}.
+ * All other values are converted via {@link #convertToType(Object,
ClassMeta)}.
+ */
+ private <T> T parseCellValue(String val, ClassMeta<T> eType) throws
ParseException {
+ if (val == null || val.equals("null"))
+ return null;
+ // Apply trimStrings at the cell level (before type conversion)
so that the String
+ // value passed to convertToType() is already trimmed.
+ if (isTrimStrings() && val != null)
+ val = val.trim();
+ if (val.isEmpty() && eType.isCharSequence())
+ return null;
+ try {
+ return convertToType(val, eType);
+ } catch (InvalidDataConversionException e) {
+ throw new ParseException(e, "Could not convert CSV cell
value ''{0}'' to type ''{1}''.", val, eType);
+ }
+ }
+}
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvReader.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvReader.java
new file mode 100644
index 0000000000..feb5f07780
--- /dev/null
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvReader.java
@@ -0,0 +1,222 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.csv;
+
+import java.io.*;
+import java.util.*;
+
+import org.apache.juneau.parser.*;
+
+/**
+ * Specialized reader for parsing CSV input.
+ *
+ * <p>
+ * Parses CSV data according to RFC 4180 with support for:
+ * <ul>
+ * <li>Quoted fields (configurable quote character)
+ * <li>Embedded delimiter characters within quoted fields
+ * <li>Embedded newlines within quoted fields
+ * <li>Embedded quote characters escaped by doubling (RFC 4180 style)
+ * <li>Both CRLF and LF line endings
+ * <li>Optional whitespace trimming
+ * </ul>
+ *
+ * <h5 class='section'>Notes:</h5><ul>
+ * <li class='note'>This class is not intended for external use.
+ * </ul>
+ */
+public class CsvReader implements Closeable {
+
+ private final ParserReader r;
+ private final char delimiter;
+ private final char quoteChar;
+ private final boolean trimStrings;
+ private boolean eof = false;
+
+ /**
+ * Constructor.
+ *
+ * @param r The parser reader to wrap.
+ * @param delimiter The field delimiter character (typically
<js>','</js>).
+ * @param quoteChar The quote character (typically <js>'"'</js>).
+ * @param trimStrings If <jk>true</jk>, field values are trimmed of
surrounding whitespace.
+ */
+ public CsvReader(ParserReader r, char delimiter, char quoteChar,
boolean trimStrings) {
+ this.r = r;
+ this.delimiter = delimiter;
+ this.quoteChar = quoteChar;
+ this.trimStrings = trimStrings;
+ }
+
+ /**
+ * Creates a {@link CsvReader} from a {@link ParserPipe}.
+ *
+ * @param pipe The parser pipe.
+ * @param delimiter The delimiter character.
+ * @param quoteChar The quote character.
+ * @param trimStrings Whether to trim strings.
+ * @return A new {@link CsvReader}, or <jk>null</jk> if the pipe has no
input.
+ * @throws IOException Thrown by underlying stream.
+ */
+ public static CsvReader from(ParserPipe pipe, char delimiter, char
quoteChar, boolean trimStrings) throws IOException {
+ var pr = pipe.getParserReader();
+ if (pr == null)
+ return null;
+ return new CsvReader(pr, delimiter, quoteChar, trimStrings);
+ }
+
+ /**
+ * Reads a single row of CSV data, returning <jk>null</jk> at end of
input.
+ *
+ * <p>
+ * Each call advances past one record (terminated by LF, CRLF, or
end-of-file).
+ * Quoted fields may span multiple lines.
+ *
+ * @return A list of field values for the row, or <jk>null</jk> if end
of input has been reached.
+ * @throws IOException Thrown by underlying stream.
+ * @throws ParseException If the CSV is malformed (e.g. an unclosed
quoted field).
+ */
+ @SuppressWarnings("java:S3776")
+ public List<String> readRow() throws IOException, ParseException {
+ if (eof)
+ return null;
+
+ // Skip blank lines between records
+ int c = r.read();
+ while (c == '\r' || c == '\n') {
+ if (c == '\r') {
+ int next = r.read();
+ if (next != '\n' && next != -1)
+ r.unread();
+ }
+ c = r.read();
+ }
+
+ if (c == -1) {
+ eof = true;
+ return null;
+ }
+
+ r.unread();
+
+ var fields = new ArrayList<String>();
+ var field = new StringBuilder();
+
+ while (true) {
+ c = r.read();
+
+ if (c == -1) {
+ eof = true;
+ fields.add(finalize(field));
+ return fields;
+ }
+
+ if (c == quoteChar) {
+ parseQuotedField(field);
+ // After a quoted field, read the next char:
delimiter, newline, or EOF
+ int next = r.read();
+ if (next == -1) {
+ eof = true;
+ fields.add(finalize(field));
+ return fields;
+ } else if (next == delimiter) {
+ fields.add(finalize(field));
+ field = new StringBuilder();
+ } else if (next == '\r') {
+ int peek = r.read();
+ if (peek != '\n' && peek != -1)
+ r.unread();
+ fields.add(finalize(field));
+ return fields;
+ } else if (next == '\n') {
+ fields.add(finalize(field));
+ return fields;
+ } else {
+ // Lenient: treat extra content after
closing quote as unquoted
+ field.append((char) next);
+ }
+ } else if (c == delimiter) {
+ fields.add(finalize(field));
+ field = new StringBuilder();
+ } else if (c == '\r') {
+ int next = r.read();
+ if (next != '\n' && next != -1)
+ r.unread();
+ fields.add(finalize(field));
+ return fields;
+ } else if (c == '\n') {
+ fields.add(finalize(field));
+ return fields;
+ } else {
+ field.append((char) c);
+ }
+ }
+ }
+
+ /**
+ * Parses the contents of a quoted field.
+ *
+ * <p>
+ * The opening quote character has already been consumed.
+ * Handles RFC 4180 doubled-quote escaping: two consecutive quote
characters inside a quoted field
+ * produce a single literal quote character.
+ *
+ * @param field The string builder to append field content to.
+ * @throws IOException Thrown by underlying stream.
+ * @throws ParseException If end of input is reached before the closing
quote.
+ */
+ private void parseQuotedField(StringBuilder field) throws IOException,
ParseException {
+ while (true) {
+ int c = r.read();
+ if (c == -1)
+ throw new ParseException("Unterminated quoted
field in CSV input.");
+ if (c == quoteChar) {
+ int next = r.read();
+ if (next == quoteChar) {
+ // Doubled quote → literal quote
+ field.append((char) quoteChar);
+ } else {
+ // Closing quote; push back the
following character
+ if (next != -1)
+ r.unread();
+ return;
+ }
+ } else {
+ field.append((char) c);
+ }
+ }
+ }
+
+ private String finalize(StringBuilder sb) {
+ var s = sb.toString();
+ return trimStrings ? s.trim() : s;
+ }
+
+ /**
+ * Returns whether the end of input has been reached.
+ *
+ * @return <jk>true</jk> if end of input has been reached.
+ */
+ public boolean isEof() {
+ return eof;
+ }
+
+ @Override
+ public void close() throws IOException {
+ r.close();
+ }
+}
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvSerializer.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvSerializer.java
index dc5c037b60..3b9ff7639c 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvSerializer.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvSerializer.java
@@ -37,10 +37,33 @@ import org.apache.juneau.serializer.*;
* becomes a row and bean properties become columns. The first row typically
contains column headers derived
* from bean property names.
*
+ * <h5 class='section'>Data Structures Incompatible with CSV (vs. JSON):</h5>
+ * <p>
+ * CSV is a flat, tabular format. Unlike {@link
org.apache.juneau.json.JsonSerializer JSON}, which
+ * supports nested objects, arrays, and type discriminators, CSV cannot
faithfully represent:
+ * <ul>
+ * <li><b>Raw primitive arrays</b> ({@code byte[]}, {@code int[]}, etc.) —
Serialize as
+ * {@code Object.toString()} (e.g. {@code [B@12345}). <i>JSON</i>
supports arrays natively
+ * and typically uses base64 for {@code byte[]}.
+ * <li><b>Nested beans</b> — Flatten to a single row; structure is lost.
<i>JSON</i> preserves
+ * nested objects naturally ({@code {"a":{"b":"c"}}}).
+ * <li><b>Collections/arrays within beans</b> — {@code List<X>},
{@code Map<K,V>}, and
+ * array properties serialize as {@code toString()}; they cannot be
parsed back. <i>JSON</i>
+ * supports arrays and objects as first-class values.
+ * <li><b>Generic type preservation</b> — No type discriminator. <i>JSON</i>
can add {@code @type}
+ * via {@code addBeanTypes().addRootType()}.
+ * <li><b>Parent/inherited properties</b> — Hierarchy flattens; may collide
with child names.
+ * <i>JSON</i> serializes all properties in a single object without loss.
+ * <li><b>Optional wrappers</b> — May serialize as the inner value;
round-trip differs from
+ * tree formats. <i>JSON</i> handles Optional consistently as value or
null.
+ * </ul>
+ *
+ * <h5 class='section'>Best Supported:</h5>
+ * <p>
+ * Collections of flat beans or maps whose properties are primitives, strings,
numbers, enums, or dates.
+ *
* <h5 class='section'>Notes:</h5><ul>
* <li class='note'>This class is thread safe and reusable.
- * <li class='warn'>This serializer is optimized for simple tabular data
structures and may have limitations
- * with complex nested objects.
* </ul>
*
*/
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvSerializerSession.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvSerializerSession.java
index f2e642319e..a1d3238eb6 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvSerializerSession.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvSerializerSession.java
@@ -253,56 +253,83 @@ public class CsvSerializerSession extends
WriterSerializerSession {
l = Collections.singleton(o);
}
- // TODO - Doesn't support DynaBeans.
if (ne(l)) {
var firstOpt = first(l);
if (!firstOpt.isPresent())
return;
- var entryType =
getClassMetaForObject(firstOpt.get());
- if (entryType.isBean()) {
- var bm = entryType.getBeanMeta();
- var addComma = Flag.create();
+ // Apply any registered swap to the first
element to determine column structure.
+ var firstRaw = firstOpt.get();
+ var firstEntry = applySwap(firstRaw,
getClassMetaForObject(firstRaw));
+ var entryType =
getClassMetaForObject(firstEntry);
+
+ // Determine the best representation strategy.
+ // Use the BeanMeta from the entry type for
bean serialization.
+ var bm = entryType.isBean() ?
entryType.getBeanMeta() : null;
+
+ if (bm != null) {
+ // Bean or DynaBean path: header row =
property names
+ var addComma = Flag.create();
bm.getProperties().values().stream().filter(BeanPropertyMeta::canRead).forEach(x
-> {
addComma.ifSet(() ->
w.w(',')).set();
w.writeEntry(x.getName());
});
w.append('\n');
+ var readableProps =
bm.getProperties().values().stream().filter(BeanPropertyMeta::canRead).toList();
l.forEach(x -> {
var addComma2 = Flag.create();
- BeanMap<?> bean = toBeanMap(x);
-
bm.getProperties().values().stream().filter(BeanPropertyMeta::canRead).forEach(y
-> {
- addComma2.ifSet(() ->
w.w(',')).set();
- var value = y.get(bean,
y.getName());
- value =
formatIfDateOrDuration(value);
- w.writeEntry(value);
- });
+ if (x == null) {
+ // Null entry: write
null for each column
+ readableProps.forEach(y
-> {
+
addComma2.ifSet(() -> w.w(',')).set();
+
w.writeEntry(null);
+ });
+ } else {
+ // Apply swap before
extracting bean properties (e.g. surrogate swaps)
+ var swapped =
applySwap(x, getClassMetaForObject(x));
+ BeanMap<?> bean =
toBeanMap(swapped);
+ readableProps.forEach(y
-> {
+
addComma2.ifSet(() -> w.w(',')).set();
+ var value =
y.get(bean, y.getName());
+ value =
formatIfDateOrDuration(value);
+ // Use
toString() to respect trimStrings setting on String values
+ if (value
instanceof String s) value = toString(s);
+
w.writeEntry(value);
+ });
+ }
w.w('\n');
});
} else if (entryType.isMap()) {
+ // Map path: header row = map keys from
the first entry
var addComma = Flag.create();
- var first = (Map)firstOpt.get();
+ var first = (Map) firstEntry;
first.keySet().forEach(x -> {
addComma.ifSet(() ->
w.w(',')).set();
- w.writeEntry(x);
+ // Apply trimStrings to map
keys as well
+ w.writeEntry(x instanceof
String s ? toString(s) : x);
});
w.append('\n');
- l.stream().forEach(x -> {
+ l.forEach(x -> {
var addComma2 = Flag.create();
- var map = (Map)x;
+ var swapped = applySwap(x,
getClassMetaForObject(x));
+ var map = (Map) swapped;
map.values().forEach(y -> {
addComma2.ifSet(() ->
w.w(',')).set();
var value =
applySwap(y, getClassMetaForObject(y));
+ // Apply trimStrings to
map values
+ if (value instanceof
String s) value = toString(s);
w.writeEntry(value);
});
w.w('\n');
});
} else {
+ // Simple value path: single "value"
column
w.writeEntry("value");
w.append('\n');
- l.stream().forEach(x -> {
+ l.forEach(x -> {
var value = applySwap(x,
getClassMetaForObject(x));
- w.writeEntry(value);
+ // Use toString() to respect
trimStrings setting
+ w.writeEntry(value == null ?
null : toString(value));
w.w('\n');
});
}
@@ -310,6 +337,7 @@ public class CsvSerializerSession extends
WriterSerializerSession {
}
}
+
private Object formatIfDateOrDuration(Object value) {
if (value == null)
return null;
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvWriter.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvWriter.java
index fda18c7072..55f51a03a1 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvWriter.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvWriter.java
@@ -183,23 +183,39 @@ public class CsvWriter extends SerializerWriter {
/**
* Writes an entry to the writer.
*
+ * <p>
+ * Follows RFC 4180 quoting rules:
+ * <ul>
+ * <li>Values containing commas, double-quote characters, or newlines
are enclosed in double quotes.
+ * <li>Double-quote characters within a quoted field are escaped by
preceding them with another double-quote.
+ * <li>The literal string {@code null} is written unquoted; the
string value {@code "null"} is quoted.
+ * </ul>
+ *
* @param value The value to write.
*/
public void writeEntry(Object value) {
- if (value == null)
+ if (value == null) {
w("null");
- else {
+ } else {
var s = value.toString();
var mustQuote = false;
- for (var i = 0; i < s.length() && ! mustQuote; i++) {
+ for (var i = 0; i < s.length() && !mustQuote; i++) {
var c = s.charAt(i);
- if (Character.isWhitespace(c) || c == ',')
+ if (c == ',' || c == '"' || c == '\r' || c ==
'\n')
mustQuote = true;
}
- if (mustQuote)
- w('"').w(s).w('"');
- else
+ if (mustQuote) {
+ w('"');
+ for (var i = 0; i < s.length(); i++) {
+ var c = s.charAt(i);
+ if (c == '"')
+ w('"'); // RFC 4180: escape
embedded quote by doubling it
+ w(c);
+ }
+ w('"');
+ } else {
w(s);
+ }
}
}
}
\ No newline at end of file
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/YamlParser.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/YamlParser.java
index 626d2552d8..89ba21c316 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/YamlParser.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/YamlParser.java
@@ -38,9 +38,40 @@ import org.apache.juneau.parser.*;
* <p>
* Handles <c>Content-Type</c> types: <bc>application/yaml, text/yaml</bc>
*
+ * <h5 class='topic'>Description</h5>
+ * <p>
+ * This parser uses an indentation-aware state machine to parse YAML directly
into POJOs without intermediate
+ * DOM objects. It supports block-style and flow-style mappings and
sequences, plain and quoted scalars,
+ * comments, document markers, and standard YAML scalar types.
+ *
+ * <h5 class='section'>Limitations compared to JSON</h5>
+ * <p>
+ * The YAML parser has some limitations when compared to {@link
org.apache.juneau.json.JsonParser JsonParser}:
+ * <ul class='spaced-list'>
+ * <li>
+ * Maps with non-String keys ({@link Boolean}, {@link
java.util.Date}, {@link java.time.temporal.Temporal},
+ * {@link Enum}) may not round-trip correctly when the target type
is a generic {@link java.util.Map} with
+ * those key types. {@link java.util.LinkedHashMap LinkedHashMap}
and {@link java.util.TreeMap TreeMap}
+ * with {@link String} keys work reliably.
+ * <li>
+ * {@link java.util.HashMap HashMap} instances that contain a
{@code null} key can fail to round-trip in
+ * some cases; {@link java.util.LinkedHashMap LinkedHashMap} and
{@link java.util.TreeMap TreeMap} handle
+ * null keys correctly.
+ * <li>
+ * No strict vs non-strict mode; unlike JSON, there is no
equivalent to JSON's lax parsing of comments,
+ * unquoted attributes, or concatenated strings.
+ * <li>
+ * YAML's indentation-based structure requires consistent
formatting; malformed indentation can cause
+ * parsing errors where equivalent JSON would parse successfully.
+ * </ul>
+ *
* <h5 class='section'>Notes:</h5><ul>
* <li class='note'>This class is thread safe and reusable.
* </ul>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='link'><a class="doclink"
href="https://juneau.apache.org/docs/topics/YamlBasics">YAML Basics</a>
+ * </ul>
*/
@SuppressWarnings({
"java:S110", // Inheritance depth acceptable
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/YamlSerializer.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/YamlSerializer.java
index b6c95ac26e..b041d1c68e 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/YamlSerializer.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/YamlSerializer.java
@@ -89,9 +89,25 @@ import org.apache.juneau.serializer.*;
* String <jv>yaml</jv> =
<jv>serializer</jv>.serialize(<jv>someObject</jv>);
* </p>
*
+ * <h5 class='section'>Limitations compared to JSON</h5>
+ * <p>
+ * The YAML serializer has fewer configuration options than {@link
org.apache.juneau.json.JsonSerializer JsonSerializer}:
+ * <ul class='spaced-list'>
+ * <li>
+ * No compact single-line output mode; YAML is always emitted in
block-style (indentation-based) format.
+ * <li>
+ * No equivalent to JSON's simple mode or attribute quoting style
variants (single vs double quotes).
+ * <li>
+ * No strict vs lax output modes; YAML output follows a
consistent, human-readable style.
+ * </ul>
+ *
* <h5 class='section'>Notes:</h5><ul>
* <li class='note'>This class is thread safe and reusable.
* </ul>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='link'><a class="doclink"
href="https://juneau.apache.org/docs/topics/YamlBasics">YAML Basics</a>
+ * </ul>
*/
@SuppressWarnings({
"java:S110", // Inheritance depth acceptable for this class hierarchy
diff --git a/juneau-utest/src/test/java/org/apache/juneau/ComboInput.java
b/juneau-utest/src/test/java/org/apache/juneau/ComboInput.java
index 4048978985..017ea5ad4c 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/ComboInput.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/ComboInput.java
@@ -40,7 +40,7 @@ public class ComboInput<T> {
List<Class<?>> swaps = list();
final Type type;
String json, jsonT, jsonR, xml, xmlT, xmlR, xmlNs, html, htmlT, htmlR,
uon, uonT, uonR, urlEncoding,
- urlEncodingT, urlEncodingR, msgPack, msgPackT, rdfXml, rdfXmlT,
rdfXmlR;
+ urlEncodingT, urlEncodingR, msgPack, msgPackT, rdfXml, rdfXmlT,
rdfXmlR, csv, yaml, yamlT, yamlR;
List<Tuple2<Class<?>,Consumer<?>>> applies = list();
public ComboInput<T> beanContext(Consumer<BeanContext.Builder> c) {
@@ -179,6 +179,22 @@ public class ComboInput<T> {
rdfXmlR = value;
return this;
}
+ public ComboInput<T> csv(String value) {
+ csv = value;
+ return this;
+ }
+ public ComboInput<T> yaml(String value) {
+ yaml = value;
+ return this;
+ }
+ public ComboInput<T> yamlT(String value) {
+ yamlT = value;
+ return this;
+ }
+ public ComboInput<T> yamlR(String value) {
+ yamlR = value;
+ return this;
+ }
public ComboInput(
String label,
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/ComboRoundTripTest_Base.java
b/juneau-utest/src/test/java/org/apache/juneau/ComboRoundTripTest_Base.java
index 385049bb9e..e30fe12e0d 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/ComboRoundTripTest_Base.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/ComboRoundTripTest_Base.java
@@ -506,4 +506,26 @@ public abstract class ComboRoundTripTest_Base extends
TestBase {
public void g33_verifyYamlR(ComboRoundTrip_Tester<?> t) throws
Exception {
t.testParseVerify("yamlR");
}
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // CSV
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @ParameterizedTest
+ @MethodSource("testers")
+ public void h11_serializeCsv(ComboRoundTrip_Tester<?> t) throws
Exception {
+ t.testSerialize("csv");
+ }
+
+ @ParameterizedTest
+ @MethodSource("testers")
+ public void h12_parseCsv(ComboRoundTrip_Tester<?> t) throws Exception {
+ t.testParse("csv");
+ }
+
+ @ParameterizedTest
+ @MethodSource("testers")
+ public void h13_verifyCsv(ComboRoundTrip_Tester<?> t) throws Exception {
+ t.testParseVerify("csv");
+ }
}
\ No newline at end of file
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/ComboRoundTrip_Tester.java
b/juneau-utest/src/test/java/org/apache/juneau/ComboRoundTrip_Tester.java
index b51ab17fba..fdeca0be2b 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/ComboRoundTrip_Tester.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/ComboRoundTrip_Tester.java
@@ -26,6 +26,7 @@ import java.util.*;
import java.util.function.*;
import org.apache.juneau.commons.function.*;
+import org.apache.juneau.csv.*;
import org.apache.juneau.html.*;
import org.apache.juneau.json.*;
import org.apache.juneau.msgpack.*;
@@ -119,6 +120,7 @@ public class ComboRoundTrip_Tester<T> {
public Builder<T> yaml(String value) { expected.put("yaml",
value); return this; }
public Builder<T> yamlT(String value) { expected.put("yamlT",
value); return this; }
public Builder<T> yamlR(String value) { expected.put("yamlR",
value); return this; }
+ public Builder<T> csv(String value) { expected.put("csv",
value); return this; }
public ComboRoundTrip_Tester<T> build() {
return new ComboRoundTrip_Tester<>(this);
@@ -167,6 +169,7 @@ public class ComboRoundTrip_Tester<T> {
serializers.put("yaml", create(b,
YamlSerializer.DEFAULT.copy().addBeanTypes().addRootType()));
serializers.put("yamlT", create(b,
YamlSerializer.create().typePropertyName("t").addBeanTypes().addRootType()));
serializers.put("yamlR", create(b,
YamlSerializer.DEFAULT_READABLE.copy().addBeanTypes().addRootType()));
+ serializers.put("csv", create(b, CsvSerializer.create()));
parsers.put("json", create(b, JsonParser.DEFAULT.copy()));
parsers.put("jsonT", create(b,
JsonParser.create().typePropertyName("t")));
@@ -189,6 +192,7 @@ public class ComboRoundTrip_Tester<T> {
parsers.put("yaml", create(b, YamlParser.DEFAULT.copy()));
parsers.put("yamlT", create(b,
YamlParser.create().typePropertyName("t")));
parsers.put("yamlR", create(b, YamlParser.DEFAULT.copy()));
+ parsers.put("csv", create(b, CsvParser.create()));
}
private Serializer create(Builder<?> tb, Serializer.Builder sb) {
@@ -282,7 +286,7 @@ public class ComboRoundTrip_Tester<T> {
var s = serializers.get(testName);
var p = parsers.get(testName);
try {
- if (isSkipped(testName + "verify", "")) return;
+ if (isSkipped(testName + "verify",
expected.get(testName))) return;
var r = s.serializeToString(in.get());
var o = p.parse(r, type);
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/ComboSerializeTest_Base.java
b/juneau-utest/src/test/java/org/apache/juneau/ComboSerializeTest_Base.java
index 9601c8b9f4..b3a876859f 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/ComboSerializeTest_Base.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/ComboSerializeTest_Base.java
@@ -203,4 +203,44 @@ public abstract class ComboSerializeTest_Base extends
TestBase {
public void f21_serializeMsgPackT(ComboSerialize_Tester<?> t) throws
Exception {
t.testSerialize("msgPackT");
}
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // YAML
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @ParameterizedTest
+ @MethodSource("testers")
+ public void g11_serializeYaml(ComboSerialize_Tester<?> t) throws
Exception {
+ t.testSerialize("yaml");
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // YAML - 't' property
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @ParameterizedTest
+ @MethodSource("testers")
+ public void g21_serializeYamlT(ComboSerialize_Tester<?> t) throws
Exception {
+ t.testSerialize("yamlT");
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // YAML - Readable
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @ParameterizedTest
+ @MethodSource("testers")
+ public void g31_serializeYamlR(ComboSerialize_Tester<?> t) throws
Exception {
+ t.testSerialize("yamlR");
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // CSV
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @ParameterizedTest
+ @MethodSource("testers")
+ public void h11_serializeCsv(ComboSerialize_Tester<?> t) throws
Exception {
+ t.testSerialize("csv");
+ }
}
\ No newline at end of file
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/ComboSerialize_Tester.java
b/juneau-utest/src/test/java/org/apache/juneau/ComboSerialize_Tester.java
index 3b97425ca7..2011ceedc1 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/ComboSerialize_Tester.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/ComboSerialize_Tester.java
@@ -26,6 +26,7 @@ import java.util.*;
import java.util.function.*;
import org.apache.juneau.commons.function.*;
+import org.apache.juneau.csv.*;
import org.apache.juneau.html.*;
import org.apache.juneau.json.*;
import org.apache.juneau.msgpack.*;
@@ -33,6 +34,7 @@ import org.apache.juneau.serializer.*;
import org.apache.juneau.uon.*;
import org.apache.juneau.urlencoding.*;
import org.apache.juneau.xml.*;
+import org.apache.juneau.yaml.*;
/**
* Represents the input to a ComboTest.
@@ -100,6 +102,10 @@ public class ComboSerialize_Tester<T> {
public Builder<T> rdfXml(String value) { expected.put("rdfXml",
value); return this; }
public Builder<T> rdfXmlT(String value) {
expected.put("rdfXmlT", value); return this; }
public Builder<T> rdfXmlR(String value) {
expected.put("rdfXmlR", value); return this; }
+ public Builder<T> csv(String value) { expected.put("csv",
value); return this; }
+ public Builder<T> yaml(String value) { expected.put("yaml",
value); return this; }
+ public Builder<T> yamlT(String value) { expected.put("yamlT",
value); return this; }
+ public Builder<T> yamlR(String value) { expected.put("yamlR",
value); return this; }
public ComboSerialize_Tester<T> build() {
return new ComboSerialize_Tester<>(this);
@@ -138,6 +144,10 @@ public class ComboSerialize_Tester<T> {
serializers.put("urlEncR", create(b,
UrlEncodingSerializer.DEFAULT_READABLE.copy().addBeanTypes().addRootType()));
serializers.put("msgPack", create(b,
MsgPackSerializer.create().addBeanTypes().addRootType()));
serializers.put("msgPackT", create(b,
MsgPackSerializer.create().typePropertyName("t").addBeanTypes().addRootType()));
+ serializers.put("csv", create(b, CsvSerializer.create()));
+ serializers.put("yaml", create(b,
YamlSerializer.DEFAULT.copy().addBeanTypes().addRootType()));
+ serializers.put("yamlT", create(b,
YamlSerializer.create().typePropertyName("t").addBeanTypes().addRootType()));
+ serializers.put("yamlR", create(b,
YamlSerializer.DEFAULT_READABLE.copy().addBeanTypes().addRootType()));
}
private Serializer create(Builder<?> tb, Serializer.Builder sb) {
@@ -153,7 +163,7 @@ public class ComboSerialize_Tester<T> {
}
private boolean isSkipped(String testName, String expected) {
- return "SKIP".equals(expected) || skipTest.test(testName);
+ return expected == null || "SKIP".equals(expected) ||
skipTest.test(testName);
}
public void testSerialize(String testName) throws Exception {
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripAddClassAttrs_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripAddClassAttrs_Test.java
index 681eb6f2b2..fecb344d45 100755
---
a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripAddClassAttrs_Test.java
+++
b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripAddClassAttrs_Test.java
@@ -24,12 +24,14 @@ import java.util.*;
import org.apache.juneau.*;
import org.apache.juneau.annotation.*;
+import org.apache.juneau.csv.*;
import org.apache.juneau.html.*;
import org.apache.juneau.json.*;
import org.apache.juneau.msgpack.*;
import org.apache.juneau.uon.*;
import org.apache.juneau.urlencoding.*;
import org.apache.juneau.xml.*;
+import org.apache.juneau.yaml.*;
import org.junit.jupiter.params.*;
import org.junit.jupiter.params.provider.*;
@@ -78,6 +80,15 @@ class RoundTripAddClassAttrs_Test extends TestBase {
tester(9, "MsgPackSerializer.DEFAULT/MsgPackParser.DEFAULT")
.serializer(MsgPackSerializer.create().addBeanTypes().addRootType())
.parser(MsgPackParser.create().disableInterfaceProxies())
+ .build(),
+ tester(10, "Yaml - default")
+
.serializer(YamlSerializer.create().addBeanTypes().addRootType())
+ .parser(YamlParser.create().disableInterfaceProxies())
+ .build(),
+ tester(11, "Csv - default")
+ .serializer(CsvSerializer.create())
+ .skipIf(o -> o == null || (o.getClass().isArray() &&
o.getClass().getComponentType().isPrimitive()))
+ .returnOriginalObject()
.build()
};
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripBeanMaps_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripBeanMaps_Test.java
index 91f768d187..c2bf2f539e 100755
---
a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripBeanMaps_Test.java
+++
b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripBeanMaps_Test.java
@@ -29,6 +29,7 @@ import javax.xml.datatype.*;
import org.apache.juneau.*;
import org.apache.juneau.annotation.*;
import org.apache.juneau.bean.html5.*;
+import org.apache.juneau.csv.*;
import org.apache.juneau.html.*;
import org.apache.juneau.json.*;
import org.apache.juneau.json.annotation.*;
@@ -36,6 +37,7 @@ import org.apache.juneau.msgpack.*;
import org.apache.juneau.uon.*;
import org.apache.juneau.urlencoding.*;
import org.apache.juneau.xml.*;
+import org.apache.juneau.yaml.*;
import org.junit.jupiter.params.*;
import org.junit.jupiter.params.provider.*;
@@ -117,6 +119,15 @@ class RoundTripBeanMaps_Test extends TestBase {
.serializer(JsonSchemaSerializer.create().keepNullProperties().addBeanTypes().addRootType())
.returnOriginalObject()
.build(),
+ tester(17, "Yaml - default")
+
.serializer(YamlSerializer.create().keepNullProperties().addBeanTypes().addRootType())
+ .parser(YamlParser.create())
+ .build(),
+ tester(18, "Csv - default")
+ .serializer(CsvSerializer.create().keepNullProperties())
+ .skipIf(o -> o == null || (o.getClass().isArray() &&
o.getClass().getComponentType().isPrimitive()))
+ .returnOriginalObject()
+ .build(),
};
static RoundTrip_Tester[] testers() {
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripBeansWithBuilders_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripBeansWithBuilders_Test.java
index d0450579a4..65f868abe1 100644
---
a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripBeansWithBuilders_Test.java
+++
b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripBeansWithBuilders_Test.java
@@ -25,12 +25,14 @@ import java.util.*;
import org.apache.juneau.*;
import org.apache.juneau.annotation.*;
+import org.apache.juneau.csv.*;
import org.apache.juneau.html.*;
import org.apache.juneau.json.*;
import org.apache.juneau.msgpack.*;
import org.apache.juneau.uon.*;
import org.apache.juneau.urlencoding.*;
import org.apache.juneau.xml.*;
+import org.apache.juneau.yaml.*;
import org.junit.jupiter.params.*;
import org.junit.jupiter.params.provider.*;
@@ -111,6 +113,15 @@ class RoundTripBeansWithBuilders_Test extends TestBase {
.serializer(JsonSchemaSerializer.create().keepNullProperties().addBeanTypes().addRootType())
.returnOriginalObject()
.build(),
+ tester(17, "Yaml - default")
+
.serializer(YamlSerializer.create().keepNullProperties().addBeanTypes().addRootType())
+ .parser(YamlParser.create())
+ .build(),
+ tester(18, "Csv - default")
+ .serializer(CsvSerializer.create().keepNullProperties())
+ .skipIf(o -> o == null || (o.getClass().isArray() &&
o.getClass().getComponentType().isPrimitive()))
+ .returnOriginalObject()
+ .build(),
};
static RoundTrip_Tester[] testers() {
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripDateTime_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripDateTime_Test.java
index 45ed8cce98..0e7ba1e580 100644
---
a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripDateTime_Test.java
+++
b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripDateTime_Test.java
@@ -24,6 +24,7 @@ import java.util.*;
import javax.xml.datatype.*;
import org.apache.juneau.*;
+import org.apache.juneau.csv.*;
import org.apache.juneau.html.*;
import org.apache.juneau.json.*;
import org.apache.juneau.msgpack.*;
@@ -114,6 +115,11 @@ class RoundTripDateTime_Test extends TestBase {
.serializer(YamlSerializer.create().keepNullProperties().addBeanTypes().addRootType())
.parser(YamlParser.create())
.build(),
+ tester(18, "Csv - default")
+ .serializer(CsvSerializer.create().keepNullProperties())
+ .skipIf(o -> o == null || (o.getClass().isArray() &&
o.getClass().getComponentType().isPrimitive()))
+ .returnOriginalObject()
+ .build(),
};
static RoundTrip_Tester[] testers() {
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripLargeObjects_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripLargeObjects_Test.java
index 6cc6990ea5..e84e789025 100755
---
a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripLargeObjects_Test.java
+++
b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripLargeObjects_Test.java
@@ -22,12 +22,14 @@ import static org.junit.jupiter.api.Assertions.*;
import java.util.*;
import org.apache.juneau.*;
+import org.apache.juneau.csv.*;
import org.apache.juneau.html.*;
import org.apache.juneau.json.*;
import org.apache.juneau.msgpack.*;
import org.apache.juneau.uon.*;
import org.apache.juneau.urlencoding.*;
import org.apache.juneau.xml.*;
+import org.apache.juneau.yaml.*;
import org.junit.jupiter.api.*;
import org.junit.jupiter.params.*;
import org.junit.jupiter.params.provider.*;
@@ -83,6 +85,15 @@ class RoundTripLargeObjects_Test extends TestBase {
tester(9, "MsgPack")
.serializer(MsgPackSerializer.create().keepNullProperties())
.parser(MsgPackParser.create())
+ .build(),
+ tester(10, "Yaml - default")
+
.serializer(YamlSerializer.create().keepNullProperties())
+ .parser(YamlParser.create())
+ .build(),
+ tester(11, "Csv - default")
+ .serializer(CsvSerializer.create().keepNullProperties())
+ .skipIf(o -> o == null || (o.getClass().isArray() &&
o.getClass().getComponentType().isPrimitive()))
+ .returnOriginalObject()
.build()
};
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripMaps_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripMaps_Test.java
index b7245c78ca..4554ac084a 100755
---
a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripMaps_Test.java
+++
b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripMaps_Test.java
@@ -23,6 +23,7 @@ import java.time.*;
import java.util.*;
import org.apache.juneau.*;
+import org.apache.juneau.csv.*;
import org.apache.juneau.html.*;
import org.apache.juneau.json.*;
import org.apache.juneau.msgpack.*;
@@ -31,6 +32,7 @@ import org.apache.juneau.swaps.*;
import org.apache.juneau.uon.*;
import org.apache.juneau.urlencoding.*;
import org.apache.juneau.xml.*;
+import org.apache.juneau.yaml.*;
import org.junit.jupiter.params.*;
import org.junit.jupiter.params.provider.*;
@@ -115,6 +117,16 @@ class RoundTripMaps_Test extends TestBase {
.serializer(JsonSchemaSerializer.create().keepNullProperties().addBeanTypes().addRootType())
.returnOriginalObject()
.build(),
+ tester(17, "Yaml - default")
+
.serializer(YamlSerializer.create().keepNullProperties().addBeanTypes().addRootType())
+ .parser(YamlParser.create())
+ .skipIf(o -> o instanceof java.util.HashMap)
+ .build(),
+ tester(18, "Csv - default")
+ .serializer(CsvSerializer.create().keepNullProperties())
+ .skipIf(o -> o == null || (o.getClass().isArray() &&
o.getClass().getComponentType().isPrimitive()))
+ .returnOriginalObject()
+ .build(),
};
static RoundTrip_Tester[] testers() {
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripTest_Base.java
b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripTest_Base.java
index 47a7142214..87356de200 100644
---
a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripTest_Base.java
+++
b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripTest_Base.java
@@ -16,7 +16,10 @@
*/
package org.apache.juneau.a.rttests;
+import java.util.*;
+
import org.apache.juneau.*;
+import org.apache.juneau.csv.*;
import org.apache.juneau.html.*;
import org.apache.juneau.json.*;
import org.apache.juneau.msgpack.*;
@@ -106,6 +109,14 @@ public abstract class RoundTripTest_Base extends TestBase {
.serializer(YamlSerializer.create().keepNullProperties().addBeanTypes().addRootType())
.parser(YamlParser.create())
.build(),
+ tester(18, "Csv - default")
+ .serializer(CsvSerializer.create().keepNullProperties())
+ // CSV serialization is validated here without parsing
(returnOriginalObject), analogous
+ // to the JSON schema tester. Full CSV round-trip
tests are in CsvParser_Test.
+ // Only test serialization of inputs that CSV can
represent.
+ .skipIf(o -> !isCsvSerializableInput(o))
+ .returnOriginalObject()
+ .build(),
};
static RoundTrip_Tester[] testers() {
@@ -115,4 +126,78 @@ public abstract class RoundTripTest_Base extends TestBase {
protected static RoundTrip_Tester.Builder tester(int index, String
label) {
return RoundTrip_Tester.create(index, label);
}
+
+ /**
+ * Returns true if the object can be serialized to CSV without error.
+ *
+ * <p>
+ * CSV can serialize any non-null input, but restricts to non-null,
non-array inputs
+ * to avoid serialization errors on raw byte arrays and similar types.
+ */
+ protected static boolean isCsvSerializableInput(Object o) {
+ if (o == null) return false;
+ var cls = o.getClass();
+ // Skip raw primitive arrays (byte[], char[], int[][], etc.) -
they serialize as toString()
+ if (cls.isArray() && cls.getComponentType().isPrimitive())
return false;
+ return true;
+ }
+
+ /**
+ * Returns true if the object can be faithfully round-tripped through
CSV.
+ *
+ * <p>
+ * CSV is tabular and only round-trips cleanly when:
+ * <ul>
+ * <li>The object is a non-empty {@link Collection} of flat beans or
Maps.
+ * <li>Primitive arrays, 2D arrays, scalar lists, and enum arrays are
excluded
+ * because CSV cannot unambiguously represent them during parsing.
+ * </ul>
+ */
+ protected static boolean isCsvRoundTripCompatible(Object o) {
+ if (o == null)
+ return false;
+ // Only Collections are supported; reject raw arrays of any kind
+ if (!(o instanceof Collection<?> col))
+ return false;
+ if (col.isEmpty())
+ return false;
+ var first = col.iterator().next();
+ if (first == null)
+ return false;
+ return isCsvCompatibleElement(first);
+ }
+
+ private static boolean isCsvCompatibleElement(Object elem) {
+ if (elem == null) return false;
+ var cls = elem.getClass();
+ // Reject scalars, arrays, enums, Optional, and any type that
isn't a bean or Map
+ if (cls.isPrimitive() || cls.isArray()) return false;
+ if (elem instanceof Number || elem instanceof Boolean || elem
instanceof Character) return false;
+ if (elem instanceof CharSequence || cls.isEnum()) return false;
+ if (elem instanceof java.util.Optional || elem instanceof
Collection) return false;
+ // Accept Maps only if all values are also simple types
+ if (elem instanceof Map m) {
+ return m.values().stream().allMatch(v -> v == null ||
isCsvSimpleType(v.getClass()));
+ }
+ // Reject JDK types that are not beans
+ if (cls.getName().startsWith("java.") ||
cls.getName().startsWith("javax.")) return false;
+ // For POJO beans: only accept if all public fields have simple
(flat) types
+ for (var field : cls.getFields()) {
+ if (!isCsvSimpleType(field.getType())) return false;
+ }
+ return cls.getFields().length > 0 || cls.getMethods().length >
0;
+ }
+
+ private static boolean isCsvSimpleType(Class<?> t) {
+ if (t == null) return true;
+ return t.isPrimitive()
+ || t == String.class
+ || t == Boolean.class
+ || t == Character.class
+ || Number.class.isAssignableFrom(t)
+ || t.isEnum()
+ || java.time.temporal.Temporal.class.isAssignableFrom(t)
+ || t == java.util.Date.class
+ || t == java.util.Calendar.class;
+ }
}
\ No newline at end of file
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripTransformBeans_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripTransformBeans_Test.java
index c63d1a8e0e..b12e79c136 100755
---
a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripTransformBeans_Test.java
+++
b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripTransformBeans_Test.java
@@ -31,6 +31,7 @@ import javax.xml.datatype.*;
import org.apache.juneau.*;
import org.apache.juneau.annotation.*;
import org.apache.juneau.commons.time.*;
+import org.apache.juneau.csv.*;
import org.apache.juneau.html.*;
import org.apache.juneau.json.*;
import org.apache.juneau.msgpack.*;
@@ -41,6 +42,7 @@ import org.apache.juneau.swaps.*;
import org.apache.juneau.uon.*;
import org.apache.juneau.urlencoding.*;
import org.apache.juneau.xml.*;
+import org.apache.juneau.yaml.*;
import org.junit.jupiter.params.*;
import org.junit.jupiter.params.provider.*;
@@ -126,6 +128,15 @@ class RoundTripTransformBeans_Test extends TestBase {
.serializer(JsonSchemaSerializer.create().keepNullProperties().addBeanTypes().addRootType())
.returnOriginalObject()
.build(),
+ tester(17, "Yaml - default")
+
.serializer(YamlSerializer.create().keepNullProperties().addBeanTypes().addRootType())
+ .parser(YamlParser.create())
+ .build(),
+ tester(18, "Csv - default")
+ .serializer(CsvSerializer.create().keepNullProperties())
+ .skipIf(o -> o == null || (o.getClass().isArray() &&
o.getClass().getComponentType().isPrimitive()))
+ .returnOriginalObject()
+ .build(),
};
static RoundTrip_Tester[] testers() {
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTrip_Tester.java
b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTrip_Tester.java
index d71387b257..9df8412b73 100644
---
a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTrip_Tester.java
+++
b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTrip_Tester.java
@@ -72,6 +72,14 @@ public class RoundTrip_Tester {
private Class<?>[] annotatedClasses = a();
public Builder annotatedClasses(Class<?>...value) {
annotatedClasses = value; return this; }
+ private java.util.function.Predicate<Object> skipIf;
+ /** Skip round-trip when the predicate returns true for the
test object. */
+ public Builder skipIf(java.util.function.Predicate<Object>
value) { skipIf = value; return this; }
+
+ private boolean ignoreErrors;
+ /** If true, silently return the original object when the
round-trip throws any exception. */
+ public Builder ignoreErrors() { ignoreErrors = true; return
this; }
+
public RoundTrip_Tester build() {
return new RoundTrip_Tester(this);
}
@@ -84,6 +92,8 @@ public class RoundTrip_Tester {
protected boolean returnOriginalObject;
private boolean validateXml;
public boolean debug;
+ private java.util.function.Predicate<Object> skipIf;
+ private boolean ignoreErrors;
private RoundTrip_Tester(Builder b) {
label = "[" + b.index + "] " + b.label;
@@ -108,14 +118,23 @@ public class RoundTrip_Tester {
validateXml = b.validateXml;
returnOriginalObject = b.returnOriginalObject;
debug = b.debug;
+ skipIf = b.skipIf;
+ ignoreErrors = b.ignoreErrors;
}
public <T> T roundTrip(T object, Type c, Type...args) throws Exception {
- var out = serialize(object, s);
- if (p == null)
+ if (skipIf != null && skipIf.test(object))
return object;
- var o = (T)p.parse(out, c, args);
- return (returnOriginalObject ? object : o);
+ try {
+ var out = serialize(object, s);
+ if (p == null)
+ return object;
+ var o = (T)p.parse(out, c, args);
+ return (returnOriginalObject ? object : o);
+ } catch (Exception e) {
+ if (ignoreErrors) return object;
+ throw e;
+ }
}
public <T> T roundTrip(T object) throws Exception {
@@ -123,11 +142,18 @@ public class RoundTrip_Tester {
}
public <T> T roundTrip(T object, Serializer serializer, Parser parser)
throws Exception {
- var out = serialize(object, serializer);
- if (parser == null)
+ if (skipIf != null && skipIf.test(object))
return object;
- var o = (T)parser.parse(out, object == null ? Object.class :
object.getClass());
- return (returnOriginalObject ? object : o);
+ try {
+ var out = serialize(object, serializer);
+ if (parser == null)
+ return object;
+ var o = (T)parser.parse(out, object == null ?
Object.class : object.getClass());
+ return (returnOriginalObject ? object : o);
+ } catch (Exception e) {
+ if (ignoreErrors) return object;
+ throw e;
+ }
}
public Serializer getSerializer() {
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/csv/CsvParser_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/csv/CsvParser_Test.java
new file mode 100644
index 0000000000..b41ebc320b
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/csv/CsvParser_Test.java
@@ -0,0 +1,292 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.csv;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.annotation.*;
+import org.apache.juneau.collections.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Tests for {@link CsvParser} and {@link CsvParserSession}.
+ */
+@SuppressWarnings({"unchecked", "rawtypes"})
+class CsvParser_Test extends TestBase {
+
+ /** Convenience: parse CSV into List<T>. */
+ private static <T> List<T> parseList(String csv, Class<T> elementType)
throws Exception {
+ return (List<T>) CsvParser.DEFAULT.parse(csv, List.class,
elementType);
+ }
+
+
//====================================================================================================
+ // a - Parse into collection of beans
+
//====================================================================================================
+
+ @Test void a01_parseBeanCollection() throws Exception {
+ var csv = "b,c\nb1,1\nb2,2\n";
+ var r = parseList(csv, A.class);
+ assertEquals(2, r.size());
+ assertEquals("b1", r.get(0).b);
+ assertEquals(1, r.get(0).c);
+ assertEquals("b2", r.get(1).b);
+ assertEquals(2, r.get(1).c);
+ }
+
+ @Test void a02_parseSingleBean() throws Exception {
+ var csv = "b,c\nhello,42\n";
+ var r = CsvParser.DEFAULT.parse(csv, A.class);
+ assertEquals("hello", r.b);
+ assertEquals(42, r.c);
+ }
+
+ @Test void a03_parseBeanArray() throws Exception {
+ var csv = "b,c\nfoo,10\nbar,20\n";
+ var r = CsvParser.DEFAULT.parse(csv, A[].class);
+ assertEquals(2, r.length);
+ assertEquals("foo", r[0].b);
+ assertEquals(10, r[0].c);
+ assertEquals("bar", r[1].b);
+ assertEquals(20, r[1].c);
+ }
+
+ public static class A {
+ public String b;
+ public int c;
+ }
+
+
//====================================================================================================
+ // b - Parse into collection of maps
+
//====================================================================================================
+
+ @Test void b01_parseMapCollection() throws Exception {
+ var csv = "name,value\nfoo,1\nbar,2\n";
+ var r = parseList(csv, Map.class);
+ assertEquals(2, r.size());
+ assertEquals("foo", r.get(0).get("name"));
+ assertEquals("1", r.get(0).get("value"));
+ assertEquals("bar", r.get(1).get("name"));
+ assertEquals("2", r.get(1).get("value"));
+ }
+
+ @Test void b02_parseSingleMap() throws Exception {
+ var csv = "k1,k2\nv1,v2\n";
+ var r = (Map<?, ?>) CsvParser.DEFAULT.parse(csv, Map.class);
+ assertEquals("v1", r.get("k1"));
+ assertEquals("v2", r.get("k2"));
+ }
+
+
//====================================================================================================
+ // c - Parse simple value collections (single "value" column)
+
//====================================================================================================
+
+ @Test void c01_parseStringList() throws Exception {
+ var csv = "value\nalpha\nbeta\ngamma\n";
+ var r = parseList(csv, String.class);
+ assertEquals(List.of("alpha", "beta", "gamma"), r);
+ }
+
+ @Test void c02_parseIntegerList() throws Exception {
+ var csv = "value\n1\n2\n3\n";
+ var r = parseList(csv, Integer.class);
+ assertEquals(List.of(1, 2, 3), r);
+ }
+
+ @Test void c03_parseBooleanList() throws Exception {
+ var csv = "value\ntrue\nfalse\ntrue\n";
+ var r = parseList(csv, Boolean.class);
+ assertEquals(List.of(true, false, true), r);
+ }
+
+
//====================================================================================================
+ // d - Null and empty value handling
+
//====================================================================================================
+
+ @Test void d01_parseNullValues() throws Exception {
+ var csv = "b,c\nnull,1\nb2,null\n";
+ var r = parseList(csv, B.class);
+ assertEquals(2, r.size());
+ assertNull(r.get(0).b);
+ assertEquals(1, (int) r.get(0).c);
+ assertEquals("b2", r.get(1).b);
+ assertNull(r.get(1).c);
+ }
+
+ public static class B {
+ public String b;
+ public Integer c;
+ }
+
+ @Test void d02_parseEmptyInput() throws Exception {
+ var r1 = CsvParser.DEFAULT.parse("", A.class);
+ assertNull(r1);
+ }
+
+ @Test void d03_parseHeaderOnly() throws Exception {
+ var csv = "b,c\n";
+ var r = parseList(csv, A.class);
+ assertTrue(r.isEmpty());
+ }
+
+
//====================================================================================================
+ // e - Quoted field handling (RFC 4180)
+
//====================================================================================================
+
+ @Test void e01_parseQuotedComma() throws Exception {
+ var csv = "value\n\"hello, world\"\n";
+ var r = parseList(csv, String.class);
+ assertEquals("hello, world", r.get(0));
+ }
+
+ @Test void e02_parseQuotedNewline() throws Exception {
+ var csv = "value\n\"line1\nline2\"\n";
+ var r = parseList(csv, String.class);
+ assertEquals("line1\nline2", r.get(0));
+ }
+
+ @Test void e03_parseDoubledQuote() throws Exception {
+ var csv = "value\n\"say \"\"hello\"\"\"\n";
+ var r = parseList(csv, String.class);
+ assertEquals("say \"hello\"", r.get(0));
+ }
+
+
//====================================================================================================
+ // f - Enum values
+
//====================================================================================================
+
+ @Test void f01_parseEnumValues() throws Exception {
+ var csv = "name,status\nTask1,PENDING\nTask2,COMPLETED\n";
+ var r = parseList(csv, C.class);
+ assertEquals(2, r.size());
+ assertEquals("Task1", r.get(0).name);
+ assertEquals(Status.PENDING, r.get(0).status);
+ assertEquals("Task2", r.get(1).name);
+ assertEquals(Status.COMPLETED, r.get(1).status);
+ }
+
+ public static class C {
+ public String name;
+ public Status status;
+ }
+
+ public enum Status { PENDING, IN_PROGRESS, COMPLETED }
+
+
//====================================================================================================
+ // g - Object (untyped) parsing
+
//====================================================================================================
+
+ @Test void g01_parseAsObject_multipleRows() throws Exception {
+ var csv = "a,b\n1,2\n3,4\n";
+ var r = CsvParser.DEFAULT.parse(csv, Object.class);
+ assertInstanceOf(JsonList.class, r);
+ assertEquals(2, ((JsonList) r).size());
+ }
+
+ @Test void g02_parseAsObject_singleRow() throws Exception {
+ var csv = "a,b\n1,2\n";
+ var r = CsvParser.DEFAULT.parse(csv, Object.class);
+ assertInstanceOf(JsonMap.class, r);
+ var m = (JsonMap) r;
+ assertEquals("1", m.get("a"));
+ assertEquals("2", m.get("b"));
+ }
+
+
//====================================================================================================
+ // h - Mismatch: fewer fields than headers
+
//====================================================================================================
+
+ @Test void h01_fewerFieldsThanHeaders() throws Exception {
+ // Row has fewer columns than header; missing fields are
treated as null/default.
+ var csv = "b,c\nhello\n";
+ var r = parseList(csv, A.class);
+ assertEquals(1, r.size());
+ assertEquals("hello", r.get(0).b);
+ assertEquals(0, r.get(0).c);
+ }
+
+
//====================================================================================================
+ // i - Round-trip: serialize then parse
+
//====================================================================================================
+
+ @Test void i01_roundTripBeanList() throws Exception {
+ var original = List.of(new D("alice", 30), new D("bob", 25));
+ var csv = CsvSerializer.DEFAULT.serialize(original);
+ var parsed = parseList(csv, D.class);
+ assertEquals(2, parsed.size());
+ assertEquals("alice", parsed.get(0).name);
+ assertEquals(30, parsed.get(0).age);
+ assertEquals("bob", parsed.get(1).name);
+ assertEquals(25, parsed.get(1).age);
+ }
+
+ @Test void i02_roundTripStringList() throws Exception {
+ var original = List.of("foo", "bar", "baz");
+ var csv = CsvSerializer.DEFAULT.serialize(original);
+ var parsed = parseList(csv, String.class);
+ assertEquals(original, parsed);
+ }
+
+ @Test void i03_roundTripIntList() throws Exception {
+ var original = List.of(1, 2, 3);
+ var csv = CsvSerializer.DEFAULT.serialize(original);
+ var parsed = parseList(csv, Integer.class);
+ assertEquals(original, parsed);
+ }
+
+ public static class D {
+ public String name;
+ public int age;
+ public D() {}
+ public D(String name, int age) { this.name = name; this.age =
age; }
+ }
+
+
//====================================================================================================
+ // j - Bean annotations
+
//====================================================================================================
+
+ @Test void j01_parseWithBeanAnnotations() throws Exception {
+ var csv = "full_name,years\nJohn,35\n";
+ var r = parseList(csv, E.class);
+ assertEquals(1, r.size());
+ assertEquals("John", r.get(0).name);
+ assertEquals(35, r.get(0).age);
+ }
+
+ public static class E {
+ @Beanp(name = "full_name")
+ public String name;
+
+ @Beanp(name = "years")
+ public int age;
+ }
+
+
//====================================================================================================
+ // k - CRLF line endings
+
//====================================================================================================
+
+ @Test void k01_parseCrlfLineEndings() throws Exception {
+ var csv = "b,c\r\nhello,1\r\nworld,2\r\n";
+ var r = parseList(csv, A.class);
+ assertEquals(2, r.size());
+ assertEquals("hello", r.get(0).b);
+ assertEquals("world", r.get(1).b);
+ }
+
+}
diff --git a/juneau-utest/src/test/java/org/apache/juneau/csv/Csv_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/csv/Csv_Test.java
index df7a67c4da..b50007f450 100755
--- a/juneau-utest/src/test/java/org/apache/juneau/csv/Csv_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/csv/Csv_Test.java
@@ -268,4 +268,84 @@ class Csv_Test extends TestBase {
public enum Status {
PENDING, IN_PROGRESS, COMPLETED
}
+
+
//====================================================================================================
+ // Test values containing commas are quoted (RFC 4180)
+
//====================================================================================================
+ @Test void i01_specialCharComma() throws Exception {
+ var l = new LinkedList<>();
+ l.add(new F("hello, world", 1));
+ l.add(new F("plain", 2));
+
+ var r = CsvSerializer.DEFAULT.serialize(l);
+ // Value with comma must be enclosed in double quotes
+ assertTrue(r.contains("\"hello, world\""), "Expected quoted
comma value but got: " + r);
+ assertTrue(r.contains("plain"));
+ }
+
+ public static class F {
+ public String b;
+ public int c;
+ public F(String b, int c) { this.b = b; this.c = c; }
+ }
+
+
//====================================================================================================
+ // Test values containing double quotes are escaped (RFC 4180 doubling)
+
//====================================================================================================
+ @Test void i02_specialCharQuote() throws Exception {
+ var l = new LinkedList<>();
+ l.add(new F("say \"hi\"", 1));
+
+ var r = CsvSerializer.DEFAULT.serialize(l);
+ // Embedded quotes must be doubled inside a quoted field
+ assertTrue(r.contains("\"say \"\"hi\"\"\""), "Expected RFC 4180
doubled quotes but got: " + r);
+ }
+
+
//====================================================================================================
+ // Test values containing newlines are quoted
+
//====================================================================================================
+ @Test void i03_specialCharNewline() throws Exception {
+ var l = new LinkedList<>();
+ l.add(new F("line1\nline2", 1));
+
+ var r = CsvSerializer.DEFAULT.serialize(l);
+ assertTrue(r.contains("\"line1\nline2\""), "Expected quoted
newline value but got: " + r);
+ }
+
+
//====================================================================================================
+ // Test null vs "null" string distinction
+
//====================================================================================================
+ @Test void i04_nullVsNullString() throws Exception {
+ var l = new LinkedList<>();
+ l.add(new G(null, "null"));
+
+ var r = CsvSerializer.DEFAULT.serialize(l);
+ // Java null → unquoted "null"; the String "null" → quoted
"\"null\""
+ // Both serialize as: null,"null"
+ assertTrue(r.contains("null,\"null\"") ||
r.contains("null,null"),
+ "Unexpected output: " + r);
+ }
+
+ public static class G {
+ public String a;
+ public String b;
+ public G(String a, String b) { this.a = a; this.b = b; }
+ }
+
+
//====================================================================================================
+ // Test serializing empty collection
+
//====================================================================================================
+ @Test void i05_emptyCollection() throws Exception {
+ var l = new LinkedList<>();
+ var r = CsvSerializer.DEFAULT.serialize(l);
+ assertEquals("", r);
+ }
+
+
//====================================================================================================
+ // Test serializing a single bean (not in a collection)
+
//====================================================================================================
+ @Test void i06_singleBean() throws Exception {
+ var r = CsvSerializer.DEFAULT.serialize(new F("hello", 42));
+ assertEquals("b,c\nhello,42\n", r);
+ }
}
\ No newline at end of file
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/marshaller/Csv_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/marshaller/Csv_Test.java
index 677a96e9c9..b9633c9bce 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/marshaller/Csv_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/marshaller/Csv_Test.java
@@ -19,13 +19,13 @@ package org.apache.juneau.marshaller;
import static org.apache.juneau.TestUtils.*;
import static org.apache.juneau.commons.utils.CollectionUtils.*;
import static org.apache.juneau.junit.bct.BctAssertions.*;
+import static org.junit.jupiter.api.Assertions.*;
import java.io.*;
import java.util.*;
import org.apache.juneau.*;
import org.apache.juneau.collections.*;
-import org.apache.juneau.parser.*;
import org.junit.jupiter.api.*;
class Csv_Test extends TestBase{
@@ -42,14 +42,29 @@ class Csv_Test extends TestBase{
assertString(expected2, Csv.of(in2,stringWriter()));
}
- @Test void a02_from() {
- var in1 = "'foo'";
- var in2 = "{foo:'bar'}";
+ @SuppressWarnings("unchecked")
+ @Test void a02_from() throws Exception {
+ // Parser is now fully implemented.
+ var csv1 = "value\nfoo\n";
+ var csv2 = "a,b\nfoo,bar\n";
- assertThrowsWithMessage(ParseException.class, "Not
implemented.", ()->Csv.to(in1, String.class));
- assertThrowsWithMessage(ParseException.class, "Not
implemented.", ()->Csv.to(stringReader(in1), String.class));
- assertThrowsWithMessage(ParseException.class, "Not
implemented.", ()->Csv.to(in2, Map.class, String.class, String.class));
- assertThrowsWithMessage(ParseException.class, "Not
implemented.", ()->Csv.to(stringReader(in2), Map.class, String.class,
String.class));
+ // Parse a single-column list of strings
+ var r1 = (List<String>) Csv.to(csv1, List.class, String.class);
+ assertEquals(1, r1.size());
+ assertEquals("foo", r1.get(0));
+
+ // Parse from Reader
+ var r2 = (List<String>) Csv.to(stringReader(csv1), List.class,
String.class);
+ assertEquals(1, r2.size());
+
+ // Parse into a map
+ var r3 = (Map<?, ?>) Csv.to(csv2, Map.class);
+ assertEquals("foo", r3.get("a"));
+ assertEquals("bar", r3.get("b"));
+
+ // Parse from Reader into a map
+ var r4 = (Map<?, ?>) Csv.to(stringReader(csv2), Map.class);
+ assertEquals("foo", r4.get("a"));
}
//-----------------------------------------------------------------------------------------------------------------
// Helper methods