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.time.*;
025import java.time.Duration;
026import java.time.temporal.*;
027import java.util.*;
028
029import javax.xml.datatype.*;
030
031import org.apache.juneau.commons.bean.*;
032import org.apache.juneau.commons.lang.*;
033import org.apache.juneau.marshall.*;
034import org.apache.juneau.marshall.serializer.*;
035import org.apache.juneau.marshall.stream.*;
036import org.apache.juneau.marshall.swap.spi.*;
037
038/**
039 * Session object that lives for the duration of a single use of {@link CsvSerializer}.
040 *
041 * <h5 class='section'>Notes:</h5><ul>
042 *    <li class='warn'>This class is not thread safe and is typically discarded after one use.
043 * </ul>
044 *
045 */
046@SuppressWarnings({
047   "java:S110", // Inheritance depth acceptable for serializer session hierarchy
048   "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants
049   "java:S3776", // Cognitive complexity acceptable for doWrite, formatForCsvCell
050   "java:S6541", // Brain method acceptable for doWrite, formatForCsvCell
051   "rawtypes", // doWrite() casts the first entry and swapped values to a raw Map after instanceof/ClassMeta checks
052   "resource" // Internal helpers return Closeables wired into pipe lifecycle; Eclipse JDT @Owning warning is by design.
053})
054public class CsvSerializerSession extends WriterSerializerSession implements RecordWritable {
055
056   private final CsvByteArrayCellFormat byteArrayFormat;
057   private final boolean allowNestedStructures;
058   private final String nullValue;
059
060   /**
061    * Builder class.
062    */
063   public static class Builder extends WriterSerializerSession.Builder<Builder> {
064
065      private CsvByteArrayCellFormat byteArrayFormat;
066      private boolean allowNestedStructures;
067      private String nullValue;
068
069      /**
070       * Constructor
071       *
072       * @param ctx The context creating this session.
073       *    <br>Cannot be <jk>null</jk>.
074       */
075      protected Builder(CsvSerializer ctx) {
076         super(reqnn("ctx", ctx));
077         byteArrayFormat = ctx.getByteArrayFormat();
078         allowNestedStructures = ctx.isAllowNestedStructures();
079         nullValue = ctx.getNullValue();
080      }
081
082      /**
083       * Allow nested structures.
084       *
085       * @param value The new value for this property.
086       * @return This object.
087       */
088      public Builder allowNestedStructures(boolean value) {
089         allowNestedStructures = value;
090         return this;
091      }
092
093      @Override
094      public CsvSerializerSession build() {
095         return new CsvSerializerSession(this);
096      }
097
098      /**
099       * Byte array format.
100       *
101       * @param value The new value for this property.
102       * @return This object.
103       */
104      public Builder byteArrayFormat(CsvByteArrayCellFormat value) {
105         if (nn(value))
106            byteArrayFormat = value;
107         return this;
108      }
109
110      /**
111       * Null value string.
112       *
113       * @param value The new value for this property. Can be <jk>null</jk> (resets to default).
114       * @return This object.
115       */
116      public Builder nullValue(String value) {
117         nullValue = value;
118         return this;
119      }
120
121      @Override /* Overridden from Builder */
122      public Builder property(String key, Object value) {
123         if (key == null) {
124            super.property(key, value);
125            return this;
126         }
127         switch (key) {
128            case "byteArrayFormat", "CsvSerializerSession.byteArrayFormat":
129               return byteArrayFormat(cvt(value, CsvByteArrayCellFormat.class));
130            case "allowNestedStructures", "CsvSerializerSession.allowNestedStructures":
131               return allowNestedStructures(cvt(value, Boolean.class));
132            case "nullValue", "CsvSerializerSession.nullValue":
133               return nullValue(cvt(value, String.class));
134            default:
135               super.property(key, value);
136               return this;
137         }
138      }
139
140   }
141
142   /**
143    * Creates a new builder for this object.
144    *
145    * @param ctx The context creating this session.
146    *    <br>Cannot be <jk>null</jk>.
147    * @return A new builder.
148    */
149   public static Builder create(CsvSerializer ctx) {
150      return new Builder(reqnn("ctx", ctx));
151   }
152
153   /**
154    * Constructor.
155    *
156    * @param builder The builder for this object.
157    */
158   protected CsvSerializerSession(Builder builder) {
159      super(builder);
160      byteArrayFormat = builder.byteArrayFormat;
161      allowNestedStructures = builder.allowNestedStructures;
162      nullValue = or(builder.nullValue, "<NULL>");
163   }
164
165   @Override /* RecordWritable */
166   public RecordWriter writeRecords(Object output) throws IOException {
167      return RecordAdapter.arrayWriter(this, output);
168   }
169
170   @Override /* RecordWritable */
171   public boolean isRecordStreaming() {
172      return false;
173   }
174
175   /**
176    * Applies any registered object swap to the specified value.
177    *
178    * <p>
179    * If a swap is registered for the value's type, the value is transformed using the swap's
180    * {@code swap()} method before being serialized.
181    *
182    * @param value The value to potentially swap.
183    * @param type The class metadata of the value's type.
184    * @return The swapped value, or the original value if no swap is registered.
185    */
186   private Object applySwap(Object value, ClassMeta<?> type) {
187      try {
188         if (value == null || type == null)
189            return value;
190
191         ObjectSwap swap = type.getSwap(this);
192         if (nn(swap)) {
193            return swap(swap, value);
194         }
195         if (type.isDate())
196            return writeDate((Date)value, type);
197         if (type.isCalendar())
198            return writeCalendar(value, type);
199         if (type.isTemporal())
200            return writeTemporal((TemporalAccessor)value, type);
201         if (type.isDuration())
202            return writeDuration((Duration)value);
203         if (type.isPeriod())
204            return writePeriod((Period)value);
205         return value;
206      } catch (SerializeException e) {
207         throw rex(e);
208      }
209   }
210
211   @SuppressWarnings({
212      "unchecked" // Type erasure requires unchecked casts
213   })
214   @Override /* Overridden from SerializerSession */
215   protected void doWrite(SerializerPipe pipe, Object o) throws IOException, SerializeException {
216      var cm = push2("root", o, getClassMetaForObject(o));
217      if (cm == null)
218         return;
219      try {
220         try (var w = getCsvWriter(pipe)) {
221            Collection<?> l = null;
222            if (cm.isArray()) {
223            // Primitive arrays and byte[] serialize as single row (value = [1;2;3] or base64)
224            if (o.getClass().getComponentType().isPrimitive())
225               l = Collections.singletonList(o);
226            else
227               l = l((Object[])o);
228         } else if (cm.isCollection()) {
229            l = (Collection<?>)o;
230         } else if (cm.isStreamable()) {
231            // CSV must inspect first element for column headers, so materialization is unavoidable.
232            l = toListFromStreamable(o, cm);
233         } else {
234            l = Collections.singleton(o);
235         }
236
237         if (ine(l)) {
238            var firstOpt = first(l);
239            if (!firstOpt.isPresent())
240               return;
241
242            // Apply any registered swap to the first element to determine column structure.
243            var firstRaw = firstOpt.get();
244            var firstEntry = applySwap(firstRaw, getClassMetaForObject(firstRaw));
245            var entryType = getClassMetaForObject(firstEntry);
246            // If swapped type is not a bean (e.g. interface proxy), use raw type for strategy
247            if (! entryType.isBean() && firstRaw != null) {
248               var rawType = getClassMetaForObject(firstRaw);
249               if (rawType.isBean())
250                  entryType = rawType;
251            }
252
253            // Expected type for each element (for type discriminator)
254            var eType = cm.isArray() || cm.isCollection() || cm.isStreamable()
255               ? cm.getElementType()
256               : getExpectedRootType(firstRaw);
257
258            // CSV is flat; when addBeanTypes or addRootType is set, add _type column to all rows
259            var addTypeColumn = isAddBeanTypes() || isAddRootType();
260            var typeColName = addTypeColumn ? getBeanTypePropertyName(entryType) : null;
261
262            // Determine the best representation strategy.
263            // Use the BeanMeta from the entry type for bean serialization.
264            var bm = entryType.isBean() ? entryType.getBeanMeta() : null;
265
266            if (bm != null) {
267               // Bean or DynaBean path: header row = property names + optional _type
268               var addComma = Flag.create();
269               bm.getProperties().values().stream().filter(BeanPropertyMeta::canRead).forEach(x -> {
270                  addComma.ifSet(() -> w.w(',')).set();
271                  w.writeEntry(x.getName());
272               });
273               // Always append _type when addBeanTypes or addRootType to support polymorphic parsing
274               if (addTypeColumn && ine(typeColName)) {
275                  w.w(',');
276                  w.writeEntry(typeColName);
277               }
278               w.append('\n');
279               var readableProps = bm.getProperties().values().stream().filter(BeanPropertyMeta::canRead).toList();
280               l.forEach(x -> {
281                  var addComma2 = Flag.create();
282                  if (x == null) {
283                     // Null entry: write null marker for each column
284                     readableProps.forEach(y -> {
285                        addComma2.ifSet(() -> w.w(',')).set();
286                        w.writeEntry(nullValue);
287                     });
288                     if (addTypeColumn && ine(typeColName)) {
289                        w.w(',');
290                        w.writeEntry("");
291                     }
292                  } else {
293                     // Apply swap before extracting bean properties (e.g. surrogate swaps)
294                     var swapped = applySwap(x, getClassMetaForObject(x));
295                     var aType = getClassMetaForObject(swapped);
296                     // When swap yields non-bean (e.g. String), use raw object for property extraction
297                     var objForBean = aType.isBean() ? swapped : x;
298                     BeanMap<?> bean = toBeanMap(objForBean);
299                     readableProps.forEach(y -> {
300                        addComma2.ifSet(() -> w.w(',')).set();
301                        var value = y.get(bean, y.getName());
302                        value = formatIfDateOrDuration(value);
303                        value = formatForCsvCell(value);
304                        // Use toString() to respect trimStrings setting on String values
305                        if (value instanceof String value2) value = toString(value2);
306                        w.writeEntry(or(value, nullValue));
307                     });
308                     if (addTypeColumn && ine(typeColName)) {
309                        var typeName = getBeanTypeName(this, eType, aType, null);
310                        w.w(',');
311                        w.writeEntry(or(typeName, ""));
312                     }
313                  }
314                  w.w('\n');
315               });
316            } else if (entryType.isMap()) {
317               // Map path: header row = map keys from the first entry + optional _type
318               var addComma = Flag.create();
319               var first = (Map) firstEntry;
320               first.keySet().forEach(x -> {
321                  addComma.ifSet(() -> w.w(',')).set();
322                  // Apply trimStrings to map keys as well
323                  Object keyVal;
324                  if (x == null)
325                     keyVal = nullValue;
326                  else if (x instanceof String x2)
327                     keyVal = toString(x2);
328                  else
329                     keyVal = x;
330                  w.writeEntry(keyVal);
331               });
332               if (addTypeColumn && ine(typeColName)) {
333                  w.w(',');
334                  w.writeEntry(typeColName);
335               }
336               w.append('\n');
337               l.forEach(x -> {
338                  var addComma2 = Flag.create();
339                  var swapped = applySwap(x, getClassMetaForObject(x));
340                  var aType = getClassMetaForObject(swapped);
341                  var map = (Map) swapped;
342                  map.values().forEach(y -> {
343                     addComma2.ifSet(() -> w.w(',')).set();
344                     var value = applySwap(y, getClassMetaForObject(y));
345                     value = formatForCsvCell(value);
346                     // Apply trimStrings to map values
347                     if (value instanceof String value2) value = toString(value2);
348                     w.writeEntry(or(value, nullValue));
349                  });
350                  if (addTypeColumn && ine(typeColName)) {
351                     var typeName = getBeanTypeName(this, eType, aType, null);
352                     w.w(',');
353                     w.writeEntry(or(typeName, ""));
354                  }
355                  w.w('\n');
356               });
357            } else {
358               // Simple value path: single "value" column + optional _type
359               w.writeEntry("value");
360               if (addTypeColumn && ine(typeColName)) {
361                  w.w(',');
362                  w.writeEntry(typeColName);
363               }
364               w.append('\n');
365               l.forEach(x -> {
366                  var value = applySwap(x, getClassMetaForObject(x));
367                  value = formatForCsvCell(value);
368                  // Use toString() to respect trimStrings setting
369                  w.writeEntry(value != null ? toString(value) : nullValue);
370                  if (addTypeColumn && ine(typeColName)) {
371                     if (x != null) {
372                        var aType = getClassMetaForObject(x);
373                        var typeName = getBeanTypeName(this, eType, aType, null);
374                        w.w(',');
375                        w.writeEntry(or(typeName, ""));
376                     } else {
377                        w.w(',');
378                        w.writeEntry("");
379                     }
380                  }
381                  w.w('\n');
382               });
383            }
384         }
385         }
386      } finally {
387         pop();
388      }
389   }
390
391
392   private Object formatIfDateOrDuration(Object value) {
393      if (value == null)
394         return null;
395      var cm = getClassMetaForObject(value);
396      if (value instanceof Date value2)
397         return writeDate(value2, cm);
398      if (value instanceof Calendar || value instanceof javax.xml.datatype.XMLGregorianCalendar)
399         return writeCalendar(value, cm);
400      if (value instanceof TemporalAccessor value2)
401         return writeTemporal(value2, cm);
402      if (value instanceof Duration)
403         return value.toString();
404      return value;
405   }
406
407   /**
408    * Formats a value for a CSV cell, handling byte[], primitive arrays, and nested structures.
409    */
410   private Object formatForCsvCell(Object value) {
411      if (value == null)
412         return null;
413      if (allowNestedStructures) {
414         var type = getClassMetaForObject(value);
415         if (value instanceof Map || value instanceof Collection || value instanceof Object[]
416               || (type.isBean() && !(value instanceof Map)))
417            return new CsvCellSerializer(byteArrayFormat, nullValue).write(value, this);
418      }
419      if (value instanceof byte[] value2) {
420         return byteArrayFormat == CsvByteArrayCellFormat.SEMICOLON_DELIMITED
421            ? formatByteArraySemicolon(value2)
422            : base64Encode(value2);
423      }
424      if (value instanceof int[] value2) {
425         var sb = new StringBuilder();
426         sb.append('[');
427         for (var i = 0; i < value2.length; i++) {
428            if (i > 0) sb.append(';');
429            sb.append(value2[i]);
430         }
431         sb.append(']');
432         return sb.toString();
433      }
434      if (value instanceof long[] value2) {
435         var sb = new StringBuilder();
436         sb.append('[');
437         for (var i = 0; i < value2.length; i++) {
438            if (i > 0) sb.append(';');
439            sb.append(value2[i]);
440         }
441         sb.append(']');
442         return sb.toString();
443      }
444      if (value instanceof double[] value2) {
445         var sb = new StringBuilder();
446         sb.append('[');
447         for (var i = 0; i < value2.length; i++) {
448            if (i > 0) sb.append(';');
449            sb.append(value2[i]);
450         }
451         sb.append(']');
452         return sb.toString();
453      }
454      if (value instanceof float[] value2) {
455         var sb = new StringBuilder();
456         sb.append('[');
457         for (var i = 0; i < value2.length; i++) {
458            if (i > 0) sb.append(';');
459            sb.append(value2[i]);
460         }
461         sb.append(']');
462         return sb.toString();
463      }
464      if (value instanceof short[] value2) {
465         var sb = new StringBuilder();
466         sb.append('[');
467         for (var i = 0; i < value2.length; i++) {
468            if (i > 0) sb.append(';');
469            sb.append(value2[i]);
470         }
471         sb.append(']');
472         return sb.toString();
473      }
474      if (value instanceof boolean[] value2) {
475         var sb = new StringBuilder();
476         sb.append('[');
477         for (var i = 0; i < value2.length; i++) {
478            if (i > 0) sb.append(';');
479            sb.append(value2[i]);
480         }
481         sb.append(']');
482         return sb.toString();
483      }
484      if (value instanceof char[] value2) {
485         var sb = new StringBuilder();
486         sb.append('[');
487         for (var i = 0; i < value2.length; i++) {
488            if (i > 0) sb.append(';');
489            sb.append((int) value2[i]);
490         }
491         sb.append(']');
492         return sb.toString();
493      }
494      return value;
495   }
496
497   private static String formatByteArraySemicolon(byte[] b) {
498      var sb = new StringBuilder();
499      for (var i = 0; i < b.length; i++) {
500         if (i > 0) sb.append(';');
501         sb.append(b[i] & 0xff);
502      }
503      return sb.toString();
504   }
505
506   /**
507    * Prepares a value for inline cell serialization (bean→Map, date format, etc.).
508    * Used by {@link CsvCellSerializer}.
509    */
510   Object prepareForInlineValue(Object value) {
511      if (value == null)
512         return null;
513      var swapped = applySwap(value, getClassMetaForObject(value));
514      var type = getClassMetaForObject(swapped);
515      if (type.isBean() && !(swapped instanceof Map))
516         return toBeanMap(swapped);
517      if (swapped instanceof Date swapped2)
518         return writeDate(swapped2, type);
519      if (swapped instanceof Calendar || swapped instanceof XMLGregorianCalendar)
520         return writeCalendar(swapped, type);
521      if (swapped instanceof TemporalAccessor swapped2)
522         return writeTemporal(swapped2, type);
523      if (swapped instanceof Duration)
524         return swapped.toString();
525      return swapped;
526   }
527
528   CsvWriter getCsvWriter(SerializerPipe out) {
529      var output = out.getRawOutput();
530      if (output instanceof CsvWriter output2)
531         return output2;
532      var w = new CsvWriter(out.getWriter(), isUseWhitespace(), getMaxIndent(), getQuoteChar(), isTrimStrings(), getUriResolver());
533      out.setWriter(w);
534      return w;
535   }
536}