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}