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.*; 022 023import org.apache.juneau.commons.bean.*; 024import org.apache.juneau.commons.collections.*; 025import org.apache.juneau.marshall.*; 026import org.apache.juneau.marshall.json.*; 027import org.apache.juneau.marshall.serializer.*; 028import org.apache.juneau.marshall.stream.*; 029 030/** 031 * Serializes POJOs to TOML v1.0.0 format. 032 * 033 * <p> 034 * TOML (Tom's Obvious Minimal Language) maps unambiguously to hash tables with native typing. 035 * See <a href="https://toml.io/en/v1.0.0">TOML v1.0.0</a> specification. 036 * 037 * <h5 class='topic'>Media types</h5> 038 * <p> 039 * Produces: <bc>application/toml</bc> 040 * <br>Accepts: <bc>application/toml</bc> 041 * 042 * <h5 class='topic'>Bean-to-TOML mapping</h5> 043 * <ul class='spaced-list'> 044 * <li>Simple properties → key-value pairs 045 * <li>Nested beans → <c>[tableName]</c> sections 046 * <li>Collections of beans → <c>[[tableName]]</c> array of tables 047 * <li>Collections of simple values → <c>key = [a, b, c]</c> 048 * <li>Maps with string keys → <c>[mapName]</c> table 049 * <li>Null properties are omitted (TOML has no null type) 050 * </ul> 051 * 052 * <h5 class='section'>Example:</h5> 053 * <p class='bjava'> 054 * <jc>// Serialize a bean to TOML</jc> 055 * String <jv>toml</jv> = TomlSerializer.<jsf>DEFAULT</jsf>.write(<jv>myBean</jv>); 056 * 057 * <jc>// Serialize a map (e.g. config with nested sections)</jc> 058 * Map<String, Object> <jv>config</jv> = Map.of(<js>"name"</js>, <js>"myapp"</js>, <js>"port"</js>, 8080); 059 * <jv>toml</jv> = TomlSerializer.<jsf>DEFAULT</jsf>.write(<jv>config</jv>); 060 * 061 * <jc>// Use the marshaller for convenience</jc> 062 * <jv>toml</jv> = Toml.<jsm>of</jsm>(<jv>config</jv>); 063 * 064 * <jc>// Custom serializer with whitespace for readability</jc> 065 * TomlSerializer <jv>s</jv> = TomlSerializer.<jsm>create</jsm>().ws().build(); 066 * <jv>toml</jv> = <jv>s</jv>.write(<jv>config</jv>); 067 * </p> 068 * 069 * <h5 class='figure'>Example output (Map of name/age):</h5> 070 * <p class='bcode'> 071 * name = "Alice" 072 * age = 30 073 * </p> 074 * 075 * <h5 class='figure'>Complex (nested table + array):</h5> 076 * <p class='bcode'> 077 * name = "Alice" 078 * age = 30 079 * tags = ["a", "b", "c"] 080 * 081 * [address] 082 * street = "123 Main St" 083 * city = "Boston" 084 * state = "MA" 085 * </p> 086 * 087 * <h5 class='topic'>Limitations compared to JSON</h5> 088 * <p> 089 * TOML is a configuration format with a more restricted type system than JSON. 090 * The following features supported by {@link JsonSerializer} 091 * are not available or limited in TOML: 092 * </p> 093 * <ul class='spaced-list'> 094 * <li><b>Null values</b> — TOML has no null type. By default, null properties are omitted. 095 * Use <c>keepNullProperties()</c> to write the configured <c>nullValue</c> string (e.g. <c><NULL></c>) instead. 096 * <li><b>Map keys</b> — TOML tables only support string keys. Maps with non-string keys 097 * (e.g. <c>Map<Integer,String></c>) have keys converted via <c>toString()</c>; ensure results are valid TOML keys. 098 * Null keys are serialized as the string <c>null</c>. 099 * <li><b>Polymorphic types</b> — <c>addBeanTypes()</c> and <c>addRootType()</c> add <c>_type</c> 100 * discriminators for polymorphic parsing; use with care as TOML structure differs from JSON. 101 * <li><b>Root-level collections</b> — TOML expects a root table. Root arrays use array-of-tables 102 * (<c>[[item]]</c>) for beans/maps, or a <c>_value</c> wrapper for primitives and simple arrays. 103 * <li><b>Duplicate keys</b> — TOML forbids duplicate keys; structures that would produce duplicate 104 * keys may yield invalid TOML. 105 * </ul> 106 * 107 * <h5 class='section'>Notes:</h5><ul> 108 * <li class='note'>This class is thread safe and reusable. 109 * </ul> 110 */ 111@SuppressWarnings({ 112 "java:S110", // Depth comes from the Serializer -> WriterSerializer base chain shared by all text-format serializers 113 "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants 114 "java:S9149", // Per-format static factories intentionally shadow the parent's. 115 "resource" // Closeable resources are owned by the caller's serializer session; Eclipse JDT @Owning warning is by design. 116}) 117public class TomlSerializer extends WriterSerializer implements TomlMetaProvider, RecordWritable { 118 119 private final java.util.concurrent.ConcurrentHashMap<ClassMeta<?>,TomlClassMeta> tomlClassMetas = new java.util.concurrent.ConcurrentHashMap<>(); 120 private final java.util.concurrent.ConcurrentHashMap<BeanPropertyMeta,TomlBeanPropertyMeta> tomlBeanPropertyMetas = new java.util.concurrent.ConcurrentHashMap<>(); 121 122 /** 123 * Builder class. 124 */ 125 public static class Builder extends WriterSerializer.Builder<Builder> { 126 127 private static final Cache<HashKey,TomlSerializer> CACHE = Cache.of(HashKey.class, TomlSerializer.class).build(); 128 129 private int inlineTableThreshold = 3; 130 private boolean useInlineTables = true; 131 private boolean sortKeys = false; 132 private String nullValue = "<NULL>"; 133 134 protected Builder() { 135 produces("application/toml"); 136 accept("application/toml"); 137 } 138 139 protected Builder(Builder copyFrom) { 140 super(reqnn("copyFrom", copyFrom)); 141 inlineTableThreshold = copyFrom.inlineTableThreshold; 142 useInlineTables = copyFrom.useInlineTables; 143 sortKeys = copyFrom.sortKeys; 144 nullValue = copyFrom.nullValue; 145 } 146 147 protected Builder(TomlSerializer copyFrom) { 148 super(reqnn("copyFrom", copyFrom)); 149 inlineTableThreshold = copyFrom.inlineTableThreshold; 150 useInlineTables = copyFrom.useInlineTables; 151 sortKeys = copyFrom.sortKeys; 152 nullValue = copyFrom.nullValue; 153 } 154 155 /** 156 * String to write for null values. Parser treats values matching this as null. 157 * 158 * <p> 159 * Default is {@code <NULL>} to avoid confusion with the literal string {@code "null"}. 160 * 161 * @param value The null marker string. 162 * @return This object. 163 */ 164 public Builder nullValue(String value) { 165 nullValue = value; 166 return this; 167 } 168 169 /** 170 * Maximum properties for inline table format. 171 * 172 * @param value The threshold. 173 * @return This object. 174 */ 175 public Builder inlineTableThreshold(int value) { 176 inlineTableThreshold = value; 177 return this; 178 } 179 180 /** 181 * Use inline tables when possible. 182 * 183 * @param value The flag. 184 * @return This object. 185 */ 186 public Builder useInlineTables(boolean value) { 187 useInlineTables = value; 188 return this; 189 } 190 191 /** 192 * Sort keys alphabetically in output. 193 * 194 * @param value The flag. 195 * @return This object. 196 */ 197 public Builder sortKeys(boolean value) { 198 sortKeys = value; 199 return this; 200 } 201 202 @Override 203 public Builder useWhitespace() { 204 super.useWhitespace(); 205 return this; 206 } 207 208 @Override 209 public Builder useWhitespace(boolean value) { 210 super.useWhitespace(value); 211 return this; 212 } 213 214 @Override 215 public Builder ws() { 216 return useWhitespace(); 217 } 218 219 @Override 220 public TomlSerializer build() { 221 return cache(CACHE).build(TomlSerializer.class); 222 } 223 224 @Override 225 public Builder copy() { 226 return new Builder(this); 227 } 228 229 @Override 230 public HashKey hashKey() { 231 return HashKey.of(super.hashKey(), inlineTableThreshold, useInlineTables, sortKeys, nullValue); 232 } 233 } 234 235 /** Default serializer. */ 236 public static final TomlSerializer DEFAULT = new TomlSerializer(create()); 237 238 /** Default serializer with blank lines between sections. */ 239 public static final TomlSerializer DEFAULT_READABLE = new TomlSerializer(create().ws()); 240 241 public static Builder create() { 242 return new Builder(); 243 } 244 245 protected final int inlineTableThreshold; 246 protected final boolean useInlineTables; 247 protected final boolean sortKeys; 248 /** String written for null values. */ 249 protected final String nullValue; 250 251 /** 252 * Constructor. 253 * 254 * @param builder The builder. 255 */ 256 public TomlSerializer(Builder builder) { 257 super(builder); 258 inlineTableThreshold = builder.inlineTableThreshold; 259 useInlineTables = builder.useInlineTables; 260 sortKeys = builder.sortKeys; 261 nullValue = or(builder.nullValue, "<NULL>"); 262 } 263 264 /** 265 * Returns the string written for null values. 266 * 267 * @return The null marker. 268 */ 269 public String getNullValue() { 270 return nullValue; 271 } 272 273 @Override 274 public TomlSerializerSession.Builder createSession() { 275 return TomlSerializerSession.create(this); 276 } 277 278 @Override /* Overridden from TomlMetaProvider */ 279 public TomlBeanPropertyMeta getTomlBeanPropertyMeta(BeanPropertyMeta bpm) { 280 if (bpm == null) 281 return TomlBeanPropertyMeta.DEFAULT; 282 return tomlBeanPropertyMetas.computeIfAbsent(bpm, k -> new TomlBeanPropertyMeta(k, this)); 283 } 284 285 @Override /* Overridden from TomlMetaProvider */ 286 public TomlClassMeta getTomlClassMeta(ClassMeta<?> cm) { 287 return tomlClassMetas.computeIfAbsent(cm, k -> new TomlClassMeta(k, this)); 288 } 289 290 @Override 291 public Builder copy() { 292 return new Builder(this); 293 } 294 295 @Override 296 protected FluentMap<String,Object> properties() { 297 return super.properties() 298 .a("inlineTableThreshold", inlineTableThreshold) 299 .a("useInlineTables", useInlineTables) 300 .a("sortKeys", sortKeys) 301 .a("nullValue", nullValue); 302 } 303 304 /** 305 * Convenience delegator that opens a {@link RecordWriter} over the output using 306 * <b>default session arguments</b> (mirrors {@link #write(Object)}). 307 * 308 * <p> 309 * The real implementation lives on {@link TomlSerializerSession#writeRecords(Object)}. 310 * Callers that need request-derived configuration should call {@link #createSession()} and 311 * invoke {@link TomlSerializerSession#writeRecords(Object)} on the built session instead. 312 * 313 * @param output The output. 314 * @return A new {@link RecordWriter}. 315 * @throws IOException If a problem occurred opening the underlying output. 316 */ 317 @Override /* RecordWritable */ 318 public RecordWriter writeRecords(Object output) throws IOException { 319 return ((RecordWritable) getSession()).writeRecords(output); 320 } 321 322 @Override /* RecordWritable */ 323 public boolean isRecordStreaming() { 324 return false; 325 } 326}