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("<NULL>")} (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 — each {@link RecordWriter#write(Object) write(...)} call appends 277 * one row — 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}