001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.marshall.csv; 018 019import static org.apache.juneau.commons.utils.CollectionUtils.*; 020import static org.apache.juneau.commons.utils.Shorts.*; 021import static org.apache.juneau.commons.utils.StringUtils.*; 022 023import java.io.*; 024import java.util.*; 025 026import org.apache.juneau.commons.bean.*; 027import org.apache.juneau.marshall.*; 028import org.apache.juneau.marshall.collections.*; 029import org.apache.juneau.marshall.parser.*; 030import org.apache.juneau.marshall.stream.*; 031import org.apache.juneau.marshall.swap.spi.*; 032 033/** 034 * Session object that lives for the duration of a single use of {@link CsvParser}. 035 * 036 * <p> 037 * Parses CSV (Comma Separated Values) input into Java objects. The first row of the CSV 038 * is treated as a header row providing column names. Subsequent rows are treated as data rows. 039 * 040 * <p> 041 * The following target type mappings are supported: 042 * <ul> 043 * <li>{@code Collection<Bean>} / {@code Bean[]} — Header row provides property names; each data row becomes a bean. 044 * <li>{@code Collection<Map>} / {@code Map[]} — Header row provides map keys; each data row becomes a map. 045 * <li>{@code Collection<SimpleType>} / {@code SimpleType[]} — Single {@code value} column; each row's value is coerced to the element type. 046 * <li>Single {@code Bean} — Header row + one data row → one bean. 047 * <li>Single {@code Map} — Header row + one data row → one map. 048 * <li>{@code Object} — Returns a {@link JsonList} of {@link JsonMap} entries. 049 * </ul> 050 * 051 * <h5 class='section'>Notes:</h5><ul> 052 * <li class='warn'>This class is not thread safe and is typically discarded after one use. 053 * </ul> 054 */ 055@SuppressWarnings({ 056 "java:S3776", // Cognitive complexity acceptable for CSV parse logic; branching is inherent to format 057 "java:S6541", // Brain method acceptable for doRead; CSV parse flow is inherently sequential 058 "rawtypes", // Raw types necessary for generic Map/List handling 059 "resource", // Closeable resources are owned by the caller's parser session; Eclipse JDT @Owning warning is by design. 060 "unchecked" // Type erasure requires unchecked casts in parse logic 061}) 062public class CsvParserSession extends ReaderParserSession implements RecordReadable { 063 064 private final CsvByteArrayCellFormat byteArrayFormat; 065 private final boolean allowNestedStructures; 066 private final String nullValue; 067 068 /** 069 * Builder class. 070 */ 071 public static class Builder extends ReaderParserSession.Builder<Builder> { 072 073 private final CsvByteArrayCellFormat byteArrayFormat; 074 private final boolean allowNestedStructures; 075 private final String nullValue; 076 077 /** 078 * Constructor 079 * 080 * @param ctx The context creating this session. 081 */ 082 protected Builder(CsvParser ctx) { 083 super(ctx); 084 byteArrayFormat = ctx.getByteArrayFormat(); 085 allowNestedStructures = ctx.isAllowNestedStructures(); 086 nullValue = ctx.getNullValue(); 087 } 088 089 @Override 090 public CsvParserSession build() { 091 return new CsvParserSession(this); 092 } 093 094 } 095 096 /** 097 * Creates a new builder for this object. 098 * 099 * @param ctx The context creating this session. 100 * @return A new builder. 101 */ 102 public static Builder create(CsvParser ctx) { 103 return new Builder(ctx); 104 } 105 106 /** 107 * Constructor. 108 * 109 * @param builder The builder for this object. 110 */ 111 protected CsvParserSession(Builder builder) { 112 super(builder); 113 byteArrayFormat = builder.byteArrayFormat; 114 allowNestedStructures = builder.allowNestedStructures; 115 nullValue = or(builder.nullValue, "<NULL>"); 116 } 117 118 @Override /* RecordReadable */ 119 public RecordReader readRecords(Object input) throws IOException { 120 return RecordAdapter.arrayReader(this, input); 121 } 122 123 @Override /* RecordReadable */ 124 public boolean isRecordStreaming() { 125 return false; 126 } 127 128 @Override /* Overridden from ParserSession */ 129 protected <T> T doRead(ParserPipe pipe, ClassMeta<T> type) throws IOException, ParseException { 130 try (var r = CsvReader.from(pipe, ',', '"', isTrimStrings())) { 131 if (r == null) 132 return null; 133 return readAnything(type, r, getOuter(), null); 134 } 135 } 136 137 /** 138 * Core parse dispatch. 139 * 140 * <p> 141 * Reads the header row, then dispatches to the appropriate parsing strategy based on the 142 * target type. 143 */ 144 private <T> T readAnything(ClassMeta<T> eType, CsvReader r, Object outer, BeanPropertyMeta pMeta) throws IOException, ParseException { 145 if (eType == null) 146 eType = (ClassMeta<T>) object(); 147 148 var swap = (ObjectSwap<T,Object>) eType.getSwap(this); 149 var builder = (BuilderSwap<T,Object>) eType.getBuilderSwap(this); 150 ClassMeta<?> sType; 151 if (builder != null) 152 sType = builder.getBuilderClassMeta(this); 153 else if (swap != null) 154 sType = swap.getSwapClassMeta(this); 155 else 156 sType = eType; 157 158 if (sType.isOptional()) 159 return (T) o(readAnything(eType.getElementType(), r, outer, pMeta)); 160 161 // Read header row 162 var headers = r.readRow(); 163 if (ie(headers)) 164 return null; 165 166 Object o = null; 167 168 if (sType.isArray()) { 169 var elementType = sType.getElementType(); 170 var list = list(); 171 for (var row = r.readRow(); row != null; row = r.readRow()) 172 list.add(readRow(headers, row, elementType, list)); 173 o = toArray(sType, list); 174 175 } else if (sType.isCollection()) { 176 var elementType = sType.getElementType(); 177 Collection<Object> l = sType.canCreateNewInstance(outer) ? (Collection<Object>) sType.newInstance() : l(); 178 for (var row = r.readRow(); row != null; row = r.readRow()) 179 l.add(readRow(headers, row, elementType, l)); 180 o = l; 181 182 } else if (sType.isBean()) { 183 var row = r.readRow(); 184 if (row != null) 185 o = readRowIntoBean(headers, row, sType, outer); 186 187 } else if (sType.isMap()) { 188 var row = r.readRow(); 189 if (row != null) 190 o = readRowIntoMap(headers, row, sType, outer); 191 192 } else if (sType.isObject()) { 193 // For Object target type: return a list of maps (or a single map if one row) 194 var results = newGenericList(); 195 for (var row = r.readRow(); row != null; row = r.readRow()) { 196 var m = newGenericMap(); 197 for (var i = 0; i < headers.size(); i++) { 198 var val = i < row.size() ? row.get(i) : null; 199 m.put(headers.get(i), readCellValue(val, object())); 200 } 201 results.add(m); 202 } 203 if (!results.isEmpty()) { 204 if (results.size() == 1) 205 o = results.get(0); 206 else 207 o = results; 208 } 209 } else { 210 // For simple target types (String, Number, Boolean, etc.) that are not beans/maps/collections, 211 // treat CSV as a single "value" column. Read the first data row's value column. 212 var valueColIdx = headers.indexOf("value"); 213 if (valueColIdx < 0) valueColIdx = 0; 214 var row = r.readRow(); 215 if (row != null && valueColIdx < row.size()) 216 o = readCellValue(row.get(valueColIdx), sType); 217 } 218 219 if (builder != null && o != null) 220 o = builder.build(this, o, eType); 221 222 if (swap != null && o != null) 223 o = unswap(swap, o, eType); 224 225 return (T) o; 226 } 227 228 /** 229 * Parses a single data row into the appropriate element object. 230 * 231 * <p> 232 * Matches JSON behavior: for polymorphic types (interface/abstract with dictionary), parse to map 233 * first then use {@link #cast(JsonMap, BeanPropertyMeta, ClassMeta)} when <code>_type</code> is present. 234 */ 235 private Object readRow(List<String> headers, List<String> row, ClassMeta<?> eType, Object outer) throws ParseException { 236 if (eType == null || eType.isObject()) { 237 var m = newGenericMap(); 238 for (var i = 0; i < headers.size(); i++) { 239 var val = i < row.size() ? row.get(i) : null; 240 m.put(headers.get(i), readCellValue(val, object())); 241 } 242 return m; 243 } 244 // Polymorphic (registry + _type column): parse to map then cast - matches JSON 245 var typeColName = getBeanTypePropertyName(eType); 246 if (eType.getBeanRegistry() != null && headers.contains(typeColName)) { 247 var m = newGenericMap(); 248 for (var i = 0; i < headers.size(); i++) { 249 var val = i < row.size() ? row.get(i) : null; 250 m.put(headers.get(i), readCellValue(val, object())); 251 } 252 if (m.containsKey(typeColName)) 253 return cast(m, null, eType); 254 throw new ParseException(this, "Polymorphic type '%s' requires _type column for resolution", eType); 255 } 256 if (eType.isBean()) { 257 return readRowIntoBean(headers, row, eType, outer); 258 } 259 if (eType.isMap()) { 260 return readRowIntoMap(headers, row, eType, outer); 261 } 262 // Simple type: use the "value" column (first column) or the only column present 263 var val = row.isEmpty() ? null : row.get(0); 264 return readCellValue(val, eType); 265 } 266 267 /** 268 * Parses a single data row into a bean of the specified type. 269 * 270 * <p> 271 * When a <code>_type</code> column (or configured type property) is present with a non-empty 272 * value, the type name is resolved to a concrete class for polymorphic bean creation. 273 */ 274 private <T> T readRowIntoBean(List<String> headers, List<String> row, ClassMeta<T> eType, Object outer) throws ParseException { 275 var typeColName = getBeanTypePropertyName(eType); 276 var typeColIdx = headers.indexOf(typeColName); 277 ClassMeta<?> beanType = eType; 278 if (typeColIdx >= 0 && typeColIdx < row.size()) { 279 var typeVal = row.get(typeColIdx); 280 if (nn(typeVal) && ! typeVal.trim().isEmpty()) { 281 var resolved = getClassMeta(typeVal.trim(), null, eType); 282 if (resolved != null && resolved.isBean()) 283 beanType = resolved; 284 } 285 } 286 var m = newBeanMap(outer, beanType.inner()); 287 for (var i = 0; i < headers.size(); i++) { 288 if (i == typeColIdx) 289 continue; 290 var header = headers.get(i); 291 var val = i < row.size() ? row.get(i) : null; 292 var pm = m.getPropertyMeta(header); 293 if (pm != null) { 294 setCurrentProperty(pm); 295 var converted = readCellValue(val, (ClassMeta<?>) pm.getBeanInfo()); 296 pm.set(m, header, converted); 297 setCurrentProperty(null); 298 } else { 299 onUnknownProperty(header, m, val); 300 } 301 } 302 return (T) checkRequired(m).getBean(); 303 } 304 305 /** 306 * Parses a single data row into a map of the specified type. 307 */ 308 @SuppressWarnings({ 309 "java:S3740" // Raw Map required for generic map from ClassMeta 310 }) 311 private Object readRowIntoMap(List<String> headers, List<String> row, ClassMeta<?> eType, Object outer) throws ParseException { 312 Map m; 313 if (eType.canCreateNewInstance(outer)) 314 m = (Map) eType.newInstance(outer); 315 else 316 m = newGenericMap(); 317 var keyType = eType.getKeyType() != null ? eType.getKeyType() : string(); 318 var valueType = eType.getValueType() != null ? eType.getValueType() : object(); 319 for (var i = 0; i < headers.size(); i++) { 320 var header = headers.get(i); 321 var val = i < row.size() ? row.get(i) : null; 322 var key = convertAttrToType(m, header, keyType); 323 var value = readCellValue(val, valueType); 324 m.put(key, value); 325 } 326 return m; 327 } 328 329 /** 330 * Converts a raw CSV cell string value to the target type. 331 * 332 * <p> 333 * The unquoted literal {@code null} maps to Java {@code null}. 334 * Handles {@code byte[]} (BASE64 or semicolon-delimited) and primitive arrays {@code [1;2;3]}. 335 * All other values are converted via {@link #convertToType(Object, ClassMeta)}. 336 */ 337 private <T> T readCellValue(String val, ClassMeta<T> eType) throws ParseException { 338 if (val == null) 339 return null; 340 // Apply trimStrings at the cell level (before type conversion) 341 if (isTrimStrings()) 342 val = val.trim(); 343 if (nullValue != null && val.equals(nullValue)) 344 return null; 345 if (val.isEmpty() && eType.isCharSequence()) 346 return null; 347 try { 348 if (allowNestedStructures && !val.isEmpty()) { 349 var trimmed = isTrimStrings() ? val.trim() : val; 350 if (trimmed.startsWith("{") || trimmed.startsWith("[")) { 351 var parsed = CsvCellParser.read(trimmed, nullValue); 352 if (parsed != null) 353 return convertToType(parsed, eType); 354 } 355 } 356 var csvParsed = readCsvCellValue(val, eType); 357 if (csvParsed != null) 358 return (T) csvParsed; 359 return convertToType(val, eType); 360 } catch (InvalidDataConversionException e) { 361 throw new ParseException(e, "Could not convert CSV cell value '%s' to type '%s'.", val, eType); 362 } 363 } 364 365 /** 366 * CSV-specific parsing for byte[] and primitive arrays. Returns null if not applicable. 367 */ 368 private Object readCsvCellValue(String val, ClassMeta<?> eType) throws ParseException { 369 if (ie(val)) 370 return null; 371 if (eType.isByteArray()) { 372 if (byteArrayFormat == CsvByteArrayCellFormat.SEMICOLON_DELIMITED) { 373 var parts = val.split(";"); 374 var b = new byte[parts.length]; 375 for (var i = 0; i < parts.length; i++) { 376 b[i] = (byte) Integer.parseInt(parts[i].trim()); 377 } 378 return b; 379 } 380 return base64Decode(val); 381 } 382 if (eType.isArray() && eType.getElementType().isPrimitive()) { 383 if (!val.startsWith("[") || !val.endsWith("]")) 384 return null; 385 var inner = val.substring(1, val.length() - 1).trim(); 386 if (inner.isEmpty()) { 387 return createEmptyPrimitiveArray(eType); 388 } 389 var parts = inner.split(";"); 390 var et = eType.getElementType(); 391 if (et.is(int.class)) { 392 var a = new int[parts.length]; 393 for (var i = 0; i < parts.length; i++) 394 a[i] = Integer.parseInt(parts[i].trim()); 395 return a; 396 } 397 if (et.is(long.class)) { 398 var a = new long[parts.length]; 399 for (var i = 0; i < parts.length; i++) 400 a[i] = Long.parseLong(parts[i].trim()); 401 return a; 402 } 403 if (et.is(double.class)) { 404 var a = new double[parts.length]; 405 for (var i = 0; i < parts.length; i++) 406 a[i] = Double.parseDouble(parts[i].trim()); 407 return a; 408 } 409 if (et.is(float.class)) { 410 var a = new float[parts.length]; 411 for (var i = 0; i < parts.length; i++) 412 a[i] = Float.parseFloat(parts[i].trim()); 413 return a; 414 } 415 if (et.is(short.class)) { 416 var a = new short[parts.length]; 417 for (var i = 0; i < parts.length; i++) 418 a[i] = Short.parseShort(parts[i].trim()); 419 return a; 420 } 421 if (et.is(boolean.class)) { 422 var a = new boolean[parts.length]; 423 for (var i = 0; i < parts.length; i++) 424 a[i] = Boolean.parseBoolean(parts[i].trim()); 425 return a; 426 } 427 if (et.is(char.class)) { 428 var a = new char[parts.length]; 429 for (var i = 0; i < parts.length; i++) 430 a[i] = (char) Integer.parseInt(parts[i].trim()); 431 return a; 432 } 433 } 434 return null; 435 } 436 437 private static Object createEmptyPrimitiveArray(ClassMeta<?> arrayType) throws ParseException { 438 var et = arrayType.getElementType(); 439 if (et.is(int.class)) return new int[0]; 440 if (et.is(long.class)) return new long[0]; 441 if (et.is(double.class)) return new double[0]; 442 if (et.is(float.class)) return new float[0]; 443 if (et.is(short.class)) return new short[0]; 444 if (et.is(boolean.class)) return new boolean[0]; 445 if (et.is(char.class)) return new char[0]; 446 if (et.is(byte.class)) return new byte[0]; 447 return null; 448 } 449}