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.hocon; 018 019import static org.apache.juneau.commons.utils.Shorts.*; 020 021import java.io.*; 022import java.net.*; 023import java.util.*; 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.*; 030import org.apache.juneau.marshall.swap.spi.*; 031 032/** 033 * Serializes POJO models to HOCON (Human-Optimized Config Object Notation). 034 * 035 * <h5 class='topic'>Media types</h5> 036 * <p> 037 * Accepts <c>Accept</c> types: <bc>application/hocon</bc> 038 * <p> 039 * Produces <c>Content-Type</c> types: <bc>application/hocon</bc> 040 * 041 * <h5 class='topic'>Description</h5> 042 * <p> 043 * HOCON is a superset of JSON used extensively in the JVM ecosystem for configuration files. 044 * This serializer produces output that is valid HOCON and can be parsed back by {@link HoconParser}. 045 * 046 * <p> 047 * The conversion is as follows: 048 * <ul class='spaced-list'> 049 * <li> 050 * {@link HashMap HashMap} / bean → HOCON object with unquoted keys and <c>=</c> separator (<c>{ key = value }</c>). 051 * <li> 052 * {@link Collection Collection} / array → HOCON array (<c>[a, b, c]</c>). 053 * <li> 054 * {@link String String} (simple, no special chars) → unquoted string (<c>name = myapp</c>). 055 * <li> 056 * {@link String String} (contains special chars or is ambiguous like <c>"true"</c>, <c>"42"</c>, <c>"null"</c>) → quoted string (<c>path = "/usr/local/bin"</c>). 057 * <li> 058 * {@link String String} (multi-line) → triple-quoted string (<c>desc = """..."""</c>). 059 * <li> 060 * {@link Number Number} → number literal (<c>port = 8080</c>). 061 * <li> 062 * {@link Boolean Boolean} → <c>true</c> or <c>false</c>. 063 * <li> 064 * <c>null</c> → <c>null</c> keyword. 065 * <li> 066 * Non-primitive types (e.g. {@link Date Date}, {@link URI URI}) → converted via {@link ObjectSwap} before serialization. 067 * </ul> 068 * 069 * <h5 class='topic'>Behavior-specific subclasses</h5> 070 * <p> 071 * <ul class='spaced-list'> 072 * <li> 073 * {@link #DEFAULT} — readable, unquoted keys/values, <c>=</c> separator, root braces omitted (config-file style). 074 * <li> 075 * {@link #DEFAULT_BRACES} — same as {@link #DEFAULT} but includes root <c>{ }</c> braces. 076 * <li> 077 * {@link #DEFAULT_COMPACT} — single-line compact output, JSON-compatible. 078 * </ul> 079 * 080 * <h5 class='section'>Example:</h5> 081 * <p class='bjava'> 082 * <jc>// Serialize a bean</jc> 083 * String <jv>hocon</jv> = HoconSerializer.<jsf>DEFAULT</jsf>.write(<jv>myBean</jv>); 084 * 085 * <jc>// Create a custom serializer instance</jc> 086 * HoconSerializer <jv>s</jv> = HoconSerializer.<jsm>create</jsm>().omitRootBraces(<jk>false</jk>).build(); 087 * String <jv>hocon2</jv> = <jv>s</jv>.write(<jv>myBean</jv>); 088 * </p> 089 * 090 * <h5 class='figure'>Example output (bean, root braces omitted):</h5> 091 * <p class='bjson'> 092 * name = myapp 093 * port = 8080 094 * debug = true 095 * database { 096 * host = localhost 097 * port = 5432 098 * } 099 * tags = [web, api, rest] 100 * </p> 101 * 102 * <h5 class='figure'>Example output (with root braces — DEFAULT_BRACES):</h5> 103 * <p class='bjson'> 104 * { 105 * name = myapp 106 * port = 8080 107 * debug = true 108 * } 109 * </p> 110 * 111 * <h5 class='section'>Limitations:</h5><ul> 112 * <li class='note'>Substitutions (<c>${var}</c>) are never emitted — the serializer always writes resolved (concrete) values. 113 * <li class='note'>Value concatenation is never emitted — the serializer always writes complete strings. 114 * <li class='note'>Path expressions (<c>a.b.c = value</c>) are never emitted — the serializer always uses nested-brace form. 115 * <li class='note'><c>include</c> directives are never emitted — these require file-system access outside the scope of a serializer. 116 * </ul> 117 * 118 * <h5 class='section'>Notes:</h5><ul> 119 * <li class='note'>This class is thread safe and reusable. 120 * </ul> 121 * 122 * <h5 class='section'>See Also:</h5><ul> 123 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/HoconSupport">HOCON</a> 124 * </ul> 125 */ 126@SuppressWarnings({ 127 "java:S110", // HoconSerializer extends WriterSerializer, which sits on the shared Serializer base-class chain; the depth comes from that format-family design. 128 "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants 129 "java:S9149", // Per-format static factories intentionally shadow the parent's. 130 "resource" // Closeable resources are owned by the caller's serializer session; Eclipse JDT @Owning warning is by design. 131}) 132public class HoconSerializer extends WriterSerializer implements HoconMetaProvider, RecordWritable { 133 134 private final java.util.concurrent.ConcurrentHashMap<ClassMeta<?>,HoconClassMeta> hoconClassMetas = new java.util.concurrent.ConcurrentHashMap<>(); 135 private final java.util.concurrent.ConcurrentHashMap<BeanPropertyMeta,HoconBeanPropertyMeta> hoconBeanPropertyMetas = new java.util.concurrent.ConcurrentHashMap<>(); 136 137 /** Use = (not :) for key-value separator. */ 138 protected final boolean useEqualsSign; 139 140 /** Use unquoted strings for simple values. */ 141 protected final boolean useUnquotedStrings; 142 143 /** Use unquoted keys when safe. */ 144 protected final boolean useUnquotedKeys; 145 146 /** Omit root object braces. */ 147 protected final boolean omitRootBraces; 148 149 /** Use triple-quoted """ for strings with newlines. */ 150 protected final boolean useMultilineStrings; 151 152 /** Use newlines instead of commas between members. */ 153 protected final boolean useNewlineSeparators; 154 155 /** 156 * Builder class. 157 */ 158 public static class Builder extends WriterSerializer.Builder<Builder> { 159 160 private static final Cache<HashKey,HoconSerializer> CACHE = Cache.of(HashKey.class, HoconSerializer.class).build(); 161 162 private boolean useEqualsSign = true; 163 private boolean useUnquotedStrings = true; 164 private boolean useUnquotedKeys = true; 165 private boolean omitRootBraces = true; 166 private boolean useMultilineStrings = true; 167 private boolean useNewlineSeparators = true; 168 169 protected Builder() { 170 produces("application/hocon"); 171 accept("application/hocon"); 172 } 173 174 protected Builder(Builder copyFrom) { 175 super(reqnn("copyFrom", copyFrom)); 176 useEqualsSign = copyFrom.useEqualsSign; 177 useUnquotedStrings = copyFrom.useUnquotedStrings; 178 useUnquotedKeys = copyFrom.useUnquotedKeys; 179 omitRootBraces = copyFrom.omitRootBraces; 180 useMultilineStrings = copyFrom.useMultilineStrings; 181 useNewlineSeparators = copyFrom.useNewlineSeparators; 182 } 183 184 protected Builder(HoconSerializer copyFrom) { 185 super(reqnn("copyFrom", copyFrom)); 186 useEqualsSign = copyFrom.useEqualsSign; 187 useUnquotedStrings = copyFrom.useUnquotedStrings; 188 useUnquotedKeys = copyFrom.useUnquotedKeys; 189 omitRootBraces = copyFrom.omitRootBraces; 190 useMultilineStrings = copyFrom.useMultilineStrings; 191 useNewlineSeparators = copyFrom.useNewlineSeparators; 192 } 193 194 /** 195 * Uses = for key-value separator (HOCON convention). 196 * 197 * @param value <jk>true</jk> for <c>=</c>, <jk>false</jk> for <c>:</c>. 198 * @return This object. 199 */ 200 public Builder useEqualsSign(boolean value) { 201 useEqualsSign = value; 202 return this; 203 } 204 205 /** 206 * Enables unquoted string output when values are unambiguous. 207 * 208 * @param value <jk>true</jk> to emit unquoted strings when safe. 209 * @return This object. 210 */ 211 public Builder useUnquotedStrings(boolean value) { 212 useUnquotedStrings = value; 213 return this; 214 } 215 216 /** 217 * Enables unquoted object keys when safe. 218 * 219 * @param value <jk>true</jk> to emit unquoted keys when safe. 220 * @return This object. 221 */ 222 public Builder useUnquotedKeys(boolean value) { 223 useUnquotedKeys = value; 224 return this; 225 } 226 227 /** 228 * Omits root-level object braces <c>{}</c> in output. 229 * 230 * @param value <jk>true</jk> to omit root braces. 231 * @return This object. 232 */ 233 public Builder omitRootBraces(boolean value) { 234 omitRootBraces = value; 235 return this; 236 } 237 238 /** 239 * Enables triple-quoted strings for values containing newlines. 240 * 241 * @param value <jk>true</jk> to use <c>"""</c> blocks for newline-containing strings. 242 * @return This object. 243 */ 244 public Builder useMultilineStrings(boolean value) { 245 useMultilineStrings = value; 246 return this; 247 } 248 249 /** 250 * Uses newlines instead of commas between object members. 251 * 252 * @param value <jk>true</jk> for newline-separated members. 253 * @return This object. 254 */ 255 public Builder useNewlineSeparators(boolean value) { 256 useNewlineSeparators = value; 257 return this; 258 } 259 260 /** 261 * Creates compact output (comma-separated, single line). 262 * 263 * @return This object. 264 */ 265 public Builder compact() { 266 useNewlineSeparators = false; 267 return this; 268 } 269 270 @Override 271 public Builder useWhitespace() { 272 super.useWhitespace(); 273 return this; 274 } 275 276 @Override 277 public Builder useWhitespace(boolean value) { 278 super.useWhitespace(value); 279 return this; 280 } 281 282 @Override /* Overridden from WriterSerializer.Builder<?> */ 283 public Builder ws() { 284 return useWhitespace(); 285 } 286 287 @Override 288 public HoconSerializer build() { 289 return cache(CACHE).build(HoconSerializer.class); 290 } 291 292 @Override 293 public Builder copy() { 294 return new Builder(this); 295 } 296 297 @Override 298 public HashKey hashKey() { 299 return HashKey.of(super.hashKey(), useEqualsSign, useUnquotedStrings, useUnquotedKeys, 300 omitRootBraces, useMultilineStrings, useNewlineSeparators); 301 } 302 } 303 304 /** Default serializer (readable, root braces omitted). */ 305 public static final HoconSerializer DEFAULT = new HoconSerializer(create().ws()); 306 307 /** Default serializer with root braces. */ 308 public static final HoconSerializer DEFAULT_BRACES = new HoconSerializer(create().ws().omitRootBraces(false)); 309 310 /** Compact serializer (single line). */ 311 public static final HoconSerializer DEFAULT_COMPACT = new HoconSerializer(create().useWhitespace(false).compact()); 312 313 /** 314 * Creates a new builder. 315 * 316 * @return A new builder. 317 */ 318 public static Builder create() { 319 return new Builder(); 320 } 321 322 /** 323 * Constructor. 324 * 325 * @param builder The builder. 326 */ 327 public HoconSerializer(Builder builder) { 328 super(builder); 329 useEqualsSign = builder.useEqualsSign; 330 useUnquotedStrings = builder.useUnquotedStrings; 331 useUnquotedKeys = builder.useUnquotedKeys; 332 omitRootBraces = builder.omitRootBraces; 333 useMultilineStrings = builder.useMultilineStrings; 334 useNewlineSeparators = builder.useNewlineSeparators; 335 } 336 337 @Override 338 public HoconSerializerSession.Builder createSession() { 339 return HoconSerializerSession.create(this); 340 } 341 342 @Override 343 public Builder copy() { 344 return new Builder(this); 345 } 346 347 @Override 348 public HoconBeanPropertyMeta getHoconBeanPropertyMeta(BeanPropertyMeta bpm) { 349 if (bpm == null) 350 return HoconBeanPropertyMeta.DEFAULT; 351 return hoconBeanPropertyMetas.computeIfAbsent(bpm, k -> new HoconBeanPropertyMeta(k, this)); 352 } 353 354 @Override 355 public HoconClassMeta getHoconClassMeta(ClassMeta<?> cm) { 356 return hoconClassMetas.computeIfAbsent(cm, k -> new HoconClassMeta(k, this)); 357 } 358 359 @Override 360 protected FluentMap<String,Object> properties() { 361 return super.properties() 362 .a("useEqualsSign", useEqualsSign) 363 .a("useUnquotedStrings", useUnquotedStrings) 364 .a("useUnquotedKeys", useUnquotedKeys) 365 .a("omitRootBraces", omitRootBraces) 366 .a("useMultilineStrings", useMultilineStrings) 367 .a("useNewlineSeparators", useNewlineSeparators); 368 } 369 370 /** 371 * Convenience delegator that opens a {@link RecordWriter} over the output using 372 * <b>default session arguments</b> (mirrors {@link #write(Object)}). 373 * 374 * <p> 375 * The real implementation lives on {@link HoconSerializerSession#writeRecords(Object)}. 376 * Callers that need request-derived configuration should call {@link #createSession()} and 377 * invoke {@link HoconSerializerSession#writeRecords(Object)} on the built session instead. 378 * 379 * @param output The output. 380 * @return A new {@link RecordWriter}. 381 * @throws IOException If a problem occurred opening the underlying output. 382 */ 383 @Override /* RecordWritable */ 384 public RecordWriter writeRecords(Object output) throws IOException { 385 return ((RecordWritable) getSession()).writeRecords(output); 386 } 387 388 @Override /* RecordWritable */ 389 public boolean isRecordStreaming() { 390 return false; 391 } 392}