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}