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.Shorts.*;
020
021import java.io.*;
022import java.util.*;
023import java.util.concurrent.*;
024
025import org.apache.juneau.commons.bean.*;
026import org.apache.juneau.commons.collections.*;
027import org.apache.juneau.marshall.*;
028import org.apache.juneau.marshall.serializer.*;
029import org.apache.juneau.marshall.stream.*;
030
031/**
032 * Serializes POJO models to CSV (Comma Separated Values) format.
033 *
034 * <p>
035 * This serializer converts Java objects (primarily collections of beans) into CSV format, where each bean
036 * becomes a row and bean properties become columns. The first row typically contains column headers derived
037 * from bean property names.
038 *
039 * <h5 class='section'>JSON Parity Features (opt-in):</h5>
040 * <ul>
041 *   <li><b>Type discriminator</b> — {@link CsvSerializer.Builder#addBeanTypes() addBeanTypes()}.{@link CsvSerializer.Builder#addRootType() addRootType()}
042 *       adds a {@code _type} column for polymorphic parsing.
043 *   <li><b>Byte arrays</b> — {@link CsvSerializer.Builder#byteArrayFormat(CsvByteArrayCellFormat) byteArrayFormat(BASE64)} (default) or
044 *       {@code SEMICOLON_DELIMITED} for {@code byte[]}; primitive arrays as {@code [1;2;3]}.
045 *   <li><b>Nested structures</b> — {@link CsvSerializer.Builder#allowNestedStructures(boolean) allowNestedStructures(true)}
046 *       enables inline {@code {key:val}} and {@code [val;val]} in cells.
047 *   <li><b>Null marker</b> — {@link CsvSerializer.Builder#nullValue(String) nullValue("&lt;NULL&gt;")} (default) for unambiguous null.
048 * </ul>
049 *
050 * <h5 class='section'>Data Structures Not Supported:</h5>
051 * <ul>
052 *   <li><b>Parent/inherited properties</b> — Hierarchy flattens; may collide with child names.
053 *   <li><b>Optional wrappers</b> — May serialize as the inner value; round-trip differs from tree formats.
054 * </ul>
055 *
056 * <h5 class='section'>Best Supported:</h5>
057 * <p>
058 * Collections of flat beans or maps whose properties are primitives, strings, numbers, enums, dates,
059 * byte arrays, or (with {@code allowNestedStructures}) nested beans, maps, and lists.
060 *
061 * <h5 class='figure'>Example output (list of maps with a,b):</h5>
062 * <p class='bcode'>
063 *    a,b
064 *    foo,bar
065 * </p>
066 *
067 * <h5 class='figure'>Complex (list of beans with nested address, flattened):</h5>
068 * <p class='bcode'>
069 *    name,age,address_street,address_city,address_state,tags
070 *    Alice,30,123 Main St,Boston,MA,"a,b,c"
071 * </p>
072 *
073 * <h5 class='section'>Notes:</h5><ul>
074 *    <li class='note'>This class is thread safe and reusable.
075 * </ul>
076 *
077 */
078@SuppressWarnings({
079   "java:S110", // Inheritance depth acceptable for this class hierarchy
080   "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants
081   "java:S9149", // Per-format static factories intentionally shadow the parent's.
082   "resource" // Closeable resources are owned by the caller's serializer session; Eclipse JDT @Owning warning is by design.
083})
084public class CsvSerializer extends WriterSerializer implements CsvMetaProvider, RecordWritable {
085
086   /**
087    * Builder class.
088    */
089   public static class Builder extends WriterSerializer.Builder<Builder> {
090
091      private static final Cache<HashKey,CsvSerializer> CACHE = Cache.of(HashKey.class, CsvSerializer.class).build();
092
093      private CsvByteArrayCellFormat byteArrayFormat;
094      private boolean allowNestedStructures;
095      private String nullValue;
096
097      /**
098       * Constructor, default settings.
099       */
100      protected Builder() {
101         produces("text/csv");
102         byteArrayFormat = CsvByteArrayCellFormat.BASE64;
103         nullValue = "<NULL>";
104      }
105
106      /**
107       * Copy constructor.
108       *
109       * @param copyFrom The builder to copy from.
110       *    <br>Cannot be <jk>null</jk>.
111       */
112      protected Builder(Builder copyFrom) {
113         super(reqnn("copyFrom", copyFrom));
114         byteArrayFormat = copyFrom.byteArrayFormat;
115         allowNestedStructures = copyFrom.allowNestedStructures;
116         nullValue = copyFrom.nullValue;
117      }
118
119      /**
120       * Copy constructor.
121       *
122       * @param copyFrom The bean to copy from.
123       *    <br>Cannot be <jk>null</jk>.
124       */
125      protected Builder(CsvSerializer copyFrom) {
126         super(reqnn("copyFrom", copyFrom));
127         byteArrayFormat = copyFrom.byteArrayFormat;
128         allowNestedStructures = copyFrom.allowNestedStructures;
129         nullValue = copyFrom.nullValue;
130      }
131
132      /**
133       * String to write for null values. Parser treats cells matching this as null.
134       *
135       * <p>
136       * Default is {@code <NULL>} to avoid confusion with the literal string {@code "null"}.
137       *
138       * @param value The null marker string.
139       * @return This object.
140       */
141      public Builder nullValue(String value) {
142         nullValue = value;
143         return this;
144      }
145
146      /**
147       * Enables inline {@code {key:val}} and {@code [val;val]} notation in cells for nested beans, maps, and lists.
148       *
149       * @param value Whether to allow nested structures.
150       * @return This object.
151       */
152      public Builder allowNestedStructures(boolean value) {
153         allowNestedStructures = value;
154         return this;
155      }
156
157      /**
158       * Format for serializing {@code byte[]} arrays in CSV cells.
159       *
160       * <p>
161       * Default is {@link CsvByteArrayCellFormat#BASE64} (matches JSON).
162       *
163       * @param value The format to use.
164       * @return This object.
165       */
166      public Builder byteArrayFormat(CsvByteArrayCellFormat value) {
167         byteArrayFormat = value;
168         return this;
169      }
170
171      @Override /* Overridden from Context.Builder<?> */
172      public HashKey hashKey() {
173         return HashKey.of(super.hashKey(), byteArrayFormat, allowNestedStructures, nullValue);
174      }
175
176      @Override /* Overridden from Context.Builder<?> */
177      public CsvSerializer build() {
178         return cache(CACHE).build(CsvSerializer.class);
179      }
180
181      @Override /* Overridden from Context.Builder<?> */
182      public Builder copy() {
183         return new Builder(this);
184      }
185
186
187   }
188
189   /** Default serializer, all default settings.*/
190   public static final CsvSerializer DEFAULT = new CsvSerializer(create());
191
192   /**
193    * Creates a new builder for this object.
194    *
195    * @return A new builder.
196    */
197   public static Builder create() {
198      return new Builder();
199   }
200
201   private final Map<ClassMeta<?>,CsvClassMeta> csvClassMetas = new ConcurrentHashMap<>();
202   private final Map<BeanPropertyMeta,CsvBeanPropertyMeta> csvBeanPropertyMetas = new ConcurrentHashMap<>();
203   private final CsvByteArrayCellFormat byteArrayFormat;
204   private final boolean allowNestedStructures;
205   private final String nullValue;
206
207   /**
208    * Constructor.
209    *
210    * @param builder The builder for this object.
211    */
212   public CsvSerializer(Builder builder) {
213      super(builder);
214      byteArrayFormat = or(builder.byteArrayFormat, CsvByteArrayCellFormat.BASE64);
215      allowNestedStructures = builder.allowNestedStructures;
216      nullValue = or(builder.nullValue, "<NULL>");
217   }
218
219   /**
220    * Returns the format for serializing {@code byte[]} arrays in CSV cells.
221    *
222    * @return The byte array format.
223    */
224   public CsvByteArrayCellFormat getByteArrayFormat() {
225      return byteArrayFormat;
226   }
227
228   /**
229    * Returns whether inline {@code {key:val}} and {@code [val;val]} notation is enabled.
230    *
231    * @return Whether nested structures are allowed.
232    */
233   public boolean isAllowNestedStructures() {
234      return allowNestedStructures;
235   }
236
237   /**
238    * Returns the string written for null values.
239    *
240    * @return The null marker.
241    */
242   public String getNullValue() {
243      return nullValue;
244   }
245
246   @Override /* Overridden from Context */
247   public Builder copy() {
248      return new Builder(this);
249   }
250
251   @Override /* Overridden from Context */
252   public CsvSerializerSession.Builder createSession() {
253      return CsvSerializerSession.create(this);
254   }
255
256   @Override /* Overridden from CsvMetaProvider */
257   public CsvBeanPropertyMeta getCsvBeanPropertyMeta(BeanPropertyMeta bpm) {
258      if (bpm == null)
259         return CsvBeanPropertyMeta.DEFAULT;
260      return csvBeanPropertyMetas.computeIfAbsent(bpm, k -> new CsvBeanPropertyMeta(k.getDelegateFor(), this));
261   }
262
263   @Override /* Overridden from CsvMetaProvider */
264   public CsvClassMeta getCsvClassMeta(ClassMeta<?> cm) {
265      return csvClassMetas.computeIfAbsent(cm, k -> new CsvClassMeta(k, this));
266   }
267
268   @Override /* Overridden from Context */
269   public CsvSerializerSession getSession() { return createSession().build(); }
270
271   /**
272    * Convenience delegator that opens a {@link RecordWriter} over the output using
273    * <b>default session arguments</b> (mirrors {@link #write(Object)}).
274    *
275    * <p>
276    * CSV is naturally row-oriented &mdash; each {@link RecordWriter#write(Object) write(...)} call appends
277    * one row &mdash; but the current cursor is buffered (the full list is materialized at close); a streaming
278    * implementation is a Phase 3b deferred item, so {@link #isRecordStreaming()} returns <jk>false</jk>.
279    *
280    * <p>
281    * The real implementation lives on {@link CsvSerializerSession#writeRecords(Object)}.  Callers that
282    * need request-derived configuration should call {@link #createSession()} and invoke
283    * {@link CsvSerializerSession#writeRecords(Object)} on the built session instead.
284    *
285    * @param output The output.
286    * @return A new {@link RecordWriter}.
287    * @throws IOException If a problem occurred opening the underlying output.
288    */
289   @Override /* RecordWritable */
290   public RecordWriter writeRecords(Object output) throws IOException {
291      return getSession().writeRecords(output);
292   }
293
294   @Override /* RecordWritable */
295   public boolean isRecordStreaming() {
296      return false;
297   }
298}