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.toml; 018 019import static org.apache.juneau.commons.utils.Shorts.*; 020 021import java.io.*; 022import java.lang.reflect.*; 023import java.time.*; 024import java.time.temporal.*; 025import java.util.*; 026import java.util.function.*; 027 028import org.apache.juneau.commons.bean.*; 029import org.apache.juneau.marshall.*; 030import org.apache.juneau.marshall.serializer.*; 031import org.apache.juneau.marshall.stream.*; 032 033/** 034 * Session for serializing objects to TOML format. 035 */ 036@SuppressWarnings({ 037 "java:S110", // Inheritance depth acceptable for serializer session hierarchy 038 "java:S1172", // pMeta accepted for API consistency with callers that track property context 039 "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants 040 "java:S3776", // Cognitive complexity acceptable for serialization logic 041 "java:S6541", // Acceptable for session implementation 042 "rawtypes", // Raw types necessary for generic type handling 043 "resource", // TomlWriter lifecycle managed by SerializerPipe 044 "unused" // pMeta parameters of writeValue and isSimpleOrInlineTable are unused but kept so callers pass property context uniformly 045}) 046public class TomlSerializerSession extends WriterSerializerSession implements RecordWritable { 047 048 /** 049 * Builder for TOML serializer session. 050 */ 051 public static class Builder extends WriterSerializerSession.Builder<Builder> { 052 053 private final TomlSerializer ctx; 054 055 protected Builder(TomlSerializer ctx) { 056 super(reqnn("ctx", ctx)); 057 this.ctx = ctx; 058 } 059 060 @Override 061 public TomlSerializerSession build() { 062 return new TomlSerializerSession(this); 063 } 064 } 065 066 /** 067 * Creates a session builder. 068 * 069 * @param ctx The serializer context. 070 * <br>Cannot be <jk>null</jk>. 071 * @return The builder. 072 */ 073 public static Builder create(TomlSerializer ctx) { 074 return new Builder(reqnn("ctx", ctx)); 075 } 076 077 private final TomlSerializer ctx; 078 079 protected TomlSerializerSession(Builder builder) { 080 super(builder); 081 ctx = builder.ctx; 082 } 083 084 @Override /* RecordWritable */ 085 public RecordWriter writeRecords(Object output) throws IOException { 086 return RecordAdapter.writer(this, output); 087 } 088 089 @Override /* RecordWritable */ 090 public boolean isRecordStreaming() { 091 return false; 092 } 093 094 @Override 095 protected void doWrite(SerializerPipe out, Object o) throws IOException, SerializeException { 096 if (o == null) 097 return; 098 TomlWriter w = getTomlWriter(out); 099 writeRoot(w, o); 100 } 101 102 protected final TomlWriter getTomlWriter(SerializerPipe out) { 103 var output = out.getRawOutput(); 104 if (output instanceof TomlWriter output2) 105 return output2; 106 var w = new TomlWriter(out.getWriter(), isUseWhitespace(), getMaxIndent(), isTrimStrings(), getUriResolver()); 107 out.setWriter(w); 108 return w; 109 } 110 111 private void writeRoot(TomlWriter w, Object o) throws SerializeException { 112 ClassMeta<?> eType = getExpectedRootType(o); 113 if (eType.isBean()) { 114 writeBean(w, toBeanMap(o), ""); 115 } else if (eType.isMap()) { 116 writeMapAtRoot(w, (Map<?,?>)o, eType); 117 } else if (eType.isCollection() || eType.isArray()) { 118 Collection<?> c = eType.isArray() ? toList(eType.inner(), o) : (Collection<?>)o; 119 ClassMeta<?> elType = eType.getElementType(); 120 if (elType.isBean() || elType.isMap()) { 121 // Array of tables at root - use "item" as table name 122 for (Object item : c) { 123 w.blankLine(); 124 w.arrayOfTablesHeader("item"); 125 if (elType.isBean()) 126 writeBean(w, toBeanMap(item), "item"); 127 else 128 writeMapAsTable(w, "item", (Map)item, elType); 129 } 130 } else { 131 // Root array of simples: wrap in _value 132 w.bareKey("_value"); 133 w.w(" = "); 134 writeArray(w, c, eType); 135 w.w('\n'); 136 } 137 } else { 138 // Root primitive: wrap in _value 139 w.bareKey("_value"); 140 w.w(" = "); 141 writeValue(w, o, eType); 142 w.w('\n'); 143 } 144 } 145 146 private void writeMapAtRoot(TomlWriter w, Map<?,?> map, ClassMeta<?> type) throws SerializeException { 147 Predicate<Object> checkNull = x -> isKeepNullProperties() || nn(x); 148 forEachEntry(map, e -> { 149 String k = e.getKey() == null ? "null" : toString(e.getKey()); 150 Object v = e.getValue(); 151 if (!checkNull.test(v)) 152 return; 153 writeMapEntry(w, k, k, v, type); 154 }); 155 } 156 157 private void writeBean(TomlWriter w, BeanMap<?> m, String tablePath) throws SerializeException { 158 Predicate<Object> checkNull = x -> isKeepNullProperties() || nn(x); 159 List<Map.Entry<BeanPropertyMeta,Object>> simple = l(); 160 List<Map.Entry<BeanPropertyMeta,Object>> complex = l(); 161 162 m.forEachValue(checkNull, (pMeta, key, value, thrown) -> { 163 if (nn(thrown)) 164 onBeanGetterException(pMeta, thrown); 165 ClassMeta<?> cMeta = (ClassMeta<?>) pMeta.getBeanInfo(); 166 if (canIgnoreValue(pMeta, key, value)) 167 return; 168 ClassMeta<?> aType = value == null ? cMeta : getClassMetaForObject(value, cMeta); 169 if (isSimpleOrInlineTable(aType, value, pMeta)) { 170 simple.add(new AbstractMap.SimpleEntry<>(pMeta, value)); 171 } else { 172 complex.add(new AbstractMap.SimpleEntry<>(pMeta, value)); 173 } 174 }); 175 176 if (ctx.sortKeys) { 177 simple.sort(Comparator.comparing(e -> e.getKey().getName())); 178 complex.sort(Comparator.comparing(e -> e.getKey().getName())); 179 } 180 181 // Pass 1: simple values and inline tables 182 for (var e : simple) { 183 String key = e.getKey().getName(); 184 Object value = e.getValue(); 185 BeanPropertyMeta pMeta = e.getKey(); 186 writeKeyValue(w, key, value, pMeta); 187 } 188 189 // Pass 2: tables and array of tables 190 for (var e : complex) { 191 String key = e.getKey().getName(); 192 Object value = e.getValue(); 193 BeanPropertyMeta pMeta = e.getKey(); 194 ClassMeta<?> cMeta = (ClassMeta<?>) pMeta.getBeanInfo(); 195 String newPath = tablePath.isEmpty() ? key : tablePath + "." + key; 196 197 if (nn(value)) { 198 w.blankLine(); 199 ClassMeta<?> aType = getClassMetaForObject(value, cMeta); 200 if (aType.isBean()) { 201 w.tableHeader(newPath); 202 writeBean(w, toBeanMap(value), newPath); 203 } else if (aType.isMap()) { 204 w.tableHeader(newPath); 205 writeMapAsTable(w, newPath, (Map)value, cMeta); 206 } else { 207 // Only table-worthy collections/arrays (non-empty, all bean/map elements) reach this bucket. 208 for (Object item : tableElements(value)) { 209 w.blankLine(); 210 w.arrayOfTablesHeader(newPath); 211 if (item instanceof Map<?,?> im) 212 writeMapAsTable(w, newPath, im, getClassMetaForObject(item)); 213 else 214 writeBean(w, toBeanMap(item), newPath); 215 } 216 } 217 } else if (isKeepNullProperties()) { 218 // The "complex" bucket is classified purely from the property's declared type (cMeta), 219 // before the value itself is known, so a null-valued Map/Collection-of-bean property lands 220 // here just like a populated one -- but this branch previously only handled the non-null 221 // case, silently dropping the property (regardless of keepNullProperties()) since there's 222 // no sub-structure to write a table header for. Fall back to a plain key = <NULL> line, 223 // same as writeKeyValue already does for a null-valued simple property. 224 writeKeyValue(w, key, null, pMeta); 225 } 226 } 227 } 228 229 private void writeKeyValue(TomlWriter w, String key, Object value, BeanPropertyMeta pMeta) throws SerializeException { 230 ClassMeta<?> cMeta; 231 if (pMeta != null) 232 cMeta = (ClassMeta<?>) pMeta.getBeanInfo(); 233 else if (value != null) 234 cMeta = getClassMetaForObject(value); 235 else 236 cMeta = object(); 237 ClassMeta<?> aType = value == null ? cMeta : getClassMetaForObject(value, cMeta); 238 239 // Apply swap 240 var swap = aType.getSwap(this); 241 if (nn(swap)) { 242 value = swap(swap, value); 243 aType = swap.getSwapClassMeta(this); 244 if (aType.isObject()) 245 aType = getClassMetaForObject(value); 246 } 247 248 if (value == null && !isKeepNullProperties()) 249 return; 250 251 writeKey(w, key); 252 w.w(" = "); 253 if (value == null) 254 w.stringValue(ctx.getNullValue()); 255 else 256 writeValue(w, value, aType, pMeta); 257 w.w('\n'); 258 } 259 260 private static void writeKey(TomlWriter w, String key) { 261 if (w.isBareKey(key)) 262 w.bareKey(key); 263 else 264 w.quotedKey(key); 265 } 266 267 private void writeValue(TomlWriter w, Object value, ClassMeta<?> aType, BeanPropertyMeta pMeta) throws SerializeException { 268 if (value == null) { 269 w.stringValue(ctx.getNullValue()); 270 return; 271 } 272 // Resolve to the runtime type and apply any registered swap. Required for collection/array 273 // elements that arrive here without having gone through writeKeyValue's swap step (e.g. 274 // List<Locale> elements where the bean-property swap is on the List, not on Locale). 275 // Without this, Locale list-elements wire as Locale.toString() ("en_US") regardless of 276 // the configured LocaleFormat, breaking BCP_47 round-trips on the parser side. 277 var rType = getClassMetaForObject(value, aType); 278 var swap = rType.getSwap(this); 279 if (nn(swap)) { 280 value = swap(swap, value); 281 rType = swap.getSwapClassMeta(this); 282 if (rType.isObject()) 283 rType = getClassMetaForObject(value); 284 } 285 // Always dispatch on the runtime type: the declared/hinted type is often erased (e.g. Object for 286 // the elements of a List<Integer> whose runtime class is a raw List). 287 aType = rType; 288 if (value == null) { 289 w.stringValue(ctx.getNullValue()); 290 return; 291 } 292 // A bean or map in value position (after '=' or inside an array) can only be an inline table. 293 if (aType.isBean()) { 294 writeInlineTable(w, toBeanMap(value)); 295 } else if (aType.isMap()) { 296 writeInlineTable(w, (Map<?,?>)value); 297 } else if (aType.isNumber()) { 298 if (value instanceof Float || value instanceof Double) 299 w.floatValue(((Number)value).doubleValue()); 300 else 301 w.integerValue(((Number)value).longValue()); 302 } else if (aType.isBoolean()) { 303 w.booleanValue((Boolean)value); 304 } else if (aType.isEnum()) { 305 w.stringValue(((Enum<?>)value).name()); 306 } else if (aType.isDate()) { 307 w.w(writeDate((Date)value, aType)); 308 } else if (aType.isCalendar()) { 309 w.w(writeCalendar(value, aType)); 310 } else if (aType.isTemporal()) { 311 Class<?> inner = aType.inner(); 312 if (inner == Year.class || inner == YearMonth.class) 313 w.stringValue(writeTemporal((TemporalAccessor)value, aType)); 314 else 315 w.w(writeTemporal((TemporalAccessor)value, aType)); 316 } else if (aType.isDuration()) { 317 w.stringValue(writeDuration((Duration)value)); 318 } else if (aType.isPeriod()) { 319 w.stringValue(writePeriod((Period)value)); 320 } else if (value.getClass().isArray()) { 321 // Generalized fallback: when aType arrives erased — most commonly 322 // because the value is a typed List<T[]> bean-property element and the parent List's 323 // elementType resolved to Object (getClassMetaForObject(value, cMeta) ignores the 324 // cMeta hint when value != null and returns the raw runtime ArrayList ClassMeta) — 325 // the isCollection/isArray dispatch below doesn't fire and the value would otherwise 326 // stringify via Object.toString() ("[B@<hash>", "[I@<hash>", …). Detect the array 327 // shape from the runtime class and emit a TOML inline-array, recursing on each 328 // (boxed) element through writeValue so the per-element wire form (integer / float 329 // / bool / string) follows the standard dispatch. Subsumes the byte[]-only fix that 330 // originally landed for the BinaryFormat.NOT_SET round-trip and extends it to 331 // int[]/long[]/short[]/float[]/double[]/boolean[]/char[]/String[]/Object[] (and any 332 // nested array shape via the recursive writeValue step). At configured (non-NOT_SET) 333 // binary formats the per-property BinarySwap has already fired in writeKeyValue's 334 // swap step, so byte[] values reach this branch only at NOT_SET — a no-op for the 335 // configured-format paths. Array.get auto-boxes primitive 336 // elements, so a single runtime-typed loop covers every primitive component type. 337 int len = Array.getLength(value); 338 w.arrayStart(); 339 for (int i = 0; i < len; i++) { 340 if (i > 0) w.w(", "); 341 Object el = Array.get(value, i); 342 if (el == null) 343 w.stringValue(ctx.getNullValue()); 344 else 345 writeValue(w, el, getClassMetaForObject(el), null); 346 } 347 w.arrayEnd(); 348 } else if (aType.isCollection() || aType.isArray()) { 349 Collection<?> c = aType.isArray() ? toList(aType.inner(), value) : (Collection<?>)value; 350 ClassMeta<?> elType = aType.getElementType(); 351 // Beans/maps become inline tables inside the array; nested collections become nested arrays. 352 w.arrayStart(); 353 boolean first = true; 354 for (Object el : c) { 355 if (!first) w.w(", "); 356 first = false; 357 if (el == null) 358 w.stringValue(ctx.getNullValue()); 359 else 360 writeValue(w, el, elType, null); 361 } 362 w.arrayEnd(); 363 } else { 364 w.stringValue(toString(value)); 365 } 366 } 367 368 private void writeValue(TomlWriter w, Object value, ClassMeta<?> aType) throws SerializeException { 369 writeValue(w, value, aType, null); 370 } 371 372 private void writeInlineTable(TomlWriter w, BeanMap<?> bm) throws SerializeException { 373 w.inlineTableStart(); 374 boolean[] first = {true}; 375 Predicate<Object> checkNull = x -> isKeepNullProperties() || nn(x); 376 bm.forEachValue(checkNull, (pm, k, v, th) -> { 377 if (!first[0]) w.w(", "); 378 first[0] = false; 379 writeKey(w, k); 380 w.w(" = "); 381 writeValue(w, v, (ClassMeta<?>) pm.getBeanInfo(), pm); 382 }); 383 w.inlineTableEnd(); 384 } 385 386 private void writeInlineTable(TomlWriter w, Map<?,?> map) throws SerializeException { 387 w.inlineTableStart(); 388 boolean[] first = {true}; 389 Predicate<Object> checkNull = x -> isKeepNullProperties() || nn(x); 390 forEachEntry(map, e -> { 391 Object v = e.getValue(); 392 if (!checkNull.test(v)) 393 return; 394 if (!first[0]) w.w(", "); 395 first[0] = false; 396 writeKey(w, e.getKey() == null ? "null" : toString(e.getKey())); 397 w.w(" = "); 398 writeValue(w, v, object(), null); 399 }); 400 w.inlineTableEnd(); 401 } 402 403 /** 404 * Returns the elements of a collection/array value if it is "table-worthy" (non-empty with every element a 405 * non-null bean or map, so it can be written as a TOML array of tables), otherwise an empty list. 406 */ 407 private List<Object> tableElements(Object value) throws SerializeException { 408 List<Object> l; 409 if (value instanceof Collection<?> c) 410 l = new ArrayList<>(c); 411 else if (value != null && value.getClass().isArray() && !value.getClass().getComponentType().isPrimitive()) 412 l = l((Object[])value); 413 else 414 return Collections.emptyList(); 415 if (l.isEmpty()) 416 return l; 417 for (Object el : l) { 418 if (el instanceof Map) 419 continue; 420 if (el == null || !getClassMetaForObject(el).isBean()) 421 return Collections.emptyList(); 422 } 423 return l; 424 } 425 426 private void writeMapAsTable(TomlWriter w, String path, Map<?,?> map, ClassMeta<?> type) throws SerializeException { 427 Predicate<Object> checkNull = x -> isKeepNullProperties() || nn(x); 428 forEachEntry(map, e -> { 429 String k = e.getKey() == null ? "null" : toString(e.getKey()); 430 Object v = e.getValue(); 431 if (!checkNull.test(v)) 432 return; 433 writeMapEntry(w, path.isEmpty() ? k : path + "." + k, k, v, type); 434 }); 435 } 436 437 /** 438 * Writes a single raw-map entry, recursing into a nested {@code [table]}/{@code [[array-of-tables]]} 439 * when the value is itself a {@link Map} or a {@link Collection} of Map/bean elements -- mirroring 440 * {@link #writeBean}'s complex pass, but for raw Map values, where there's no declared bean-property 441 * generic type to consult, so table-worthiness is determined from the actual runtime elements 442 * instead (unlike a bean property, a raw Map value's Collection entries never carry element-type 443 * info to fall back on). 444 */ 445 private void writeMapEntry(TomlWriter w, String tablePath, String key, Object value, ClassMeta<?> hint) throws SerializeException { 446 if (value instanceof Map<?,?> v2) { 447 w.blankLine(); 448 w.tableHeader(tablePath); 449 writeMapAsTable(w, tablePath, v2, getClassMetaForObject(value, hint)); 450 } else if (! tableElements(value).isEmpty()) { 451 for (Object item : tableElements(value)) { 452 w.blankLine(); 453 w.arrayOfTablesHeader(tablePath); 454 if (item instanceof Map<?,?> im) 455 writeMapAsTable(w, tablePath, im, getClassMetaForObject(item)); 456 else 457 writeBean(w, toBeanMap(item), tablePath); 458 } 459 } else { 460 writeKeyValue(w, key, value, null); 461 } 462 } 463 464 private void writeArray(TomlWriter w, Collection<?> c, ClassMeta<?> type) throws SerializeException { 465 ClassMeta<?> elType = type.getElementType(); 466 w.arrayStart(); 467 boolean first = true; 468 for (Object el : c) { 469 if (!first) w.w(", "); 470 first = false; 471 writeValue(w, el, elType); 472 } 473 w.arrayEnd(); 474 } 475 476 private boolean isSimpleOrInlineTable(ClassMeta<?> aType, Object value, BeanPropertyMeta pMeta) throws SerializeException { 477 if (aType.isBean()) { 478 BeanMap<?> bm = toBeanMap(value); 479 return shouldUseInlineTable(bm); 480 } 481 if (aType.isCollectionOrArray()) 482 return tableElements(value).isEmpty(); 483 return !aType.isMap(); 484 } 485 486 private boolean shouldUseInlineTable(BeanMap<?> bm) { 487 if (!ctx.useInlineTables) 488 return false; 489 int count = 0; 490 for (BeanPropertyMeta p : bm.getMeta().getProperties().values()) { 491 if (!p.isWriteOnly()) { 492 ClassMeta<?> cm = (ClassMeta<?>) p.getBeanInfo(); 493 if (cm.isBean() || cm.isMap() || (cm.isCollectionOrArray() && cm.getElementType().isBean())) 494 return false; 495 count++; 496 if (count > ctx.inlineTableThreshold) 497 return false; 498 } 499 } 500 return count <= ctx.inlineTableThreshold; 501 } 502}