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.hjson; 018 019import static org.apache.juneau.commons.utils.Shorts.*; 020 021import java.io.*; 022import java.util.*; 023 024import org.apache.juneau.commons.bean.*; 025import org.apache.juneau.commons.collections.*; 026import org.apache.juneau.marshall.*; 027import org.apache.juneau.marshall.serializer.*; 028import org.apache.juneau.marshall.stream.*; 029import org.apache.juneau.marshall.swap.spi.*; 030 031/** 032 * Serializes POJO models to Hjson (Human JSON). 033 * 034 * <h5 class='topic'>Media types</h5> 035 * <p> 036 * Handles <c>Accept</c> types: <bc>application/hjson, application/hjson+json</bc> 037 * <p> 038 * Produces <c>Content-Type</c> types: <bc>application/hjson</bc> 039 * 040 * <h5 class='topic'>Description</h5> 041 * <p> 042 * Hjson is a syntax extension of JSON designed for human-friendly configuration files and hand-edited data. 043 * This serializer produces output that is valid Hjson and can be parsed back by {@link HjsonParser}. 044 * 045 * <p> 046 * The conversion is as follows: 047 * <ul class='spaced-list'> 048 * <li> 049 * Maps (e.g. {@link HashMap HashMaps}, {@link TreeMap TreeMaps}) are converted to Hjson objects. 050 * <li> 051 * Collections (e.g. {@link HashSet HashSets}, {@link LinkedList LinkedLists}) and Java arrays are converted to 052 * Hjson arrays. 053 * <li> 054 * {@link String Strings} are serialized as quoteless values when they contain only safe characters. 055 * Strings that would be misinterpreted as booleans, null, or numbers are automatically double-quoted. 056 * <li> 057 * {@link Number Numbers} (e.g. {@link Integer}, {@link Long}, {@link Double}) are converted to Hjson numbers. 058 * <li> 059 * {@link Boolean Booleans} are converted to {@code true} or {@code false}. 060 * <li> 061 * {@code null} values are converted to Hjson {@code null}. 062 * <li> 063 * Beans are converted to Hjson objects. 064 * </ul> 065 * 066 * <p> 067 * The types above are considered "Hjson-primitive" object types. 068 * Any non-Hjson-primitive object types are transformed into Hjson-primitive object types through 069 * {@link ObjectSwap ObjectSwaps} associated through the 070 * {@link org.apache.juneau.marshall.MarshallingContext.Builder#swaps(Class...) MarshallingContext.Builder.swaps(Class...)} method. 071 * Several default transforms are provided for transforming Dates, Enums, Iterators, etc. 072 * 073 * <h5 class='section'>Example:</h5> 074 * <p class='bjava'> 075 * <jc>// Use one of the default serializers to serialize a POJO</jc> 076 * String <jv>hjson</jv> = HjsonSerializer.<jsf>DEFAULT</jsf>.write(<jv>someObject</jv>); 077 * 078 * <jc>// Create a compact serializer (comma-separated, no newlines between members)</jc> 079 * HjsonSerializer <jv>compact</jv> = HjsonSerializer.<jsm>create</jsm>().compact().build(); 080 * 081 * <jc>// Clone an existing serializer and enable multiline strings</jc> 082 * <jv>compact</jv> = HjsonSerializer.<jsf>DEFAULT</jsf>.copy().useMultilineStrings(<jk>true</jk>).build(); 083 * </p> 084 * 085 * <h5 class='figure'>Example output - readable mode (Map of name/age):</h5> 086 * <p class='bjson'> 087 * { 088 * name: Alice 089 * age: 30 090 * } 091 * </p> 092 * 093 * <h5 class='figure'>Example output - compact mode:</h5> 094 * <p class='bjson'> 095 * {name:Alice,age:30} 096 * </p> 097 * 098 * <h5 class='section'>Notes:</h5><ul> 099 * <li class='note'>String values equal to <c>true</c>, <c>false</c>, <c>null</c>, or parseable as numbers 100 * are automatically serialized in double quotes to prevent parse ambiguity. 101 * <li class='note'>Empty strings are always serialized as <c>""</c>. 102 * <li class='note'>Root-level objects always include enclosing braces <c>{}</c> by default. 103 * Use {@link Builder#omitRootBraces(boolean)} to suppress them. 104 * <li class='note'>In compact mode, strings containing newlines are serialized as double-quoted strings 105 * with JSON escape sequences rather than multiline <c>'''</c> blocks. 106 * <li class='note'>Format-specific field-level annotations ({@link Hjson @Hjson}) are supported 107 * but intentionally minimal in the initial implementation. 108 * <li class='note'>This class is thread safe and reusable. 109 * </ul> 110 * 111 * <h5 class='section'>See Also:</h5><ul> 112 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/Hjson">Hjson</a> 113 * <li class='link'><a class="doclink" href="https://hjson.github.io/syntax.html">Hjson Specification</a> 114 * </ul> 115 */ 116@SuppressWarnings({ 117 "java:S110", // Depth comes from the Serializer -> WriterSerializer base chain shared by all text-format serializers 118 "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants 119 "java:S9149", // Per-format static factories intentionally shadow the parent's. 120 "resource" // Closeable resources are owned by the caller's serializer session; Eclipse JDT @Owning warning is by design. 121}) 122public class HjsonSerializer extends WriterSerializer implements HjsonMetaProvider, RecordWritable { 123 124 private final java.util.concurrent.ConcurrentHashMap<ClassMeta<?>,HjsonClassMeta> hjsonClassMetas = new java.util.concurrent.ConcurrentHashMap<>(); 125 private final java.util.concurrent.ConcurrentHashMap<BeanPropertyMeta,HjsonBeanPropertyMeta> hjsonBeanPropertyMetas = new java.util.concurrent.ConcurrentHashMap<>(); 126 127 /** Use multiline ''' for strings with newlines. */ 128 protected final boolean useMultilineStrings; 129 130 /** Use quoteless strings for simple values. */ 131 protected final boolean useQuotelessStrings; 132 133 /** Use quoteless keys when safe. */ 134 protected final boolean useQuotelessKeys; 135 136 /** Omit root object braces. */ 137 protected final boolean omitRootBraces; 138 139 /** Use newlines instead of commas between members. */ 140 protected final boolean useNewlineSeparators; 141 142 /** 143 * Builder class. 144 */ 145 public static class Builder extends WriterSerializer.Builder<Builder> { 146 147 private static final Cache<HashKey,HjsonSerializer> CACHE = Cache.of(HashKey.class, HjsonSerializer.class).build(); 148 149 private boolean useMultilineStrings = true; 150 private boolean useQuotelessStrings = true; 151 private boolean useQuotelessKeys = true; 152 private boolean omitRootBraces = false; 153 private boolean useNewlineSeparators = true; 154 155 protected Builder() { 156 produces("application/hjson"); 157 accept("application/hjson,application/hjson+json"); 158 } 159 160 protected Builder(Builder copyFrom) { 161 super(reqnn("copyFrom", copyFrom)); 162 useMultilineStrings = copyFrom.useMultilineStrings; 163 useQuotelessStrings = copyFrom.useQuotelessStrings; 164 useQuotelessKeys = copyFrom.useQuotelessKeys; 165 omitRootBraces = copyFrom.omitRootBraces; 166 useNewlineSeparators = copyFrom.useNewlineSeparators; 167 } 168 169 protected Builder(HjsonSerializer copyFrom) { 170 super(reqnn("copyFrom", copyFrom)); 171 useMultilineStrings = copyFrom.useMultilineStrings; 172 useQuotelessStrings = copyFrom.useQuotelessStrings; 173 useQuotelessKeys = copyFrom.useQuotelessKeys; 174 omitRootBraces = copyFrom.omitRootBraces; 175 useNewlineSeparators = copyFrom.useNewlineSeparators; 176 } 177 178 /** 179 * Enables multiline strings for values containing newlines. 180 * 181 * @param value <jk>true</jk> to use <c>'''</c> blocks for newline-containing strings. 182 * @return This object. 183 */ 184 public Builder useMultilineStrings(boolean value) { 185 useMultilineStrings = value; 186 return this; 187 } 188 189 /** 190 * Enables quoteless string output when values are unambiguous. 191 * 192 * @param value <jk>true</jk> to emit unquoted strings when safe. 193 * @return This object. 194 */ 195 public Builder useQuotelessStrings(boolean value) { 196 useQuotelessStrings = value; 197 return this; 198 } 199 200 /** 201 * Enables quoteless object keys when safe. 202 * 203 * @param value <jk>true</jk> to emit unquoted keys when safe. 204 * @return This object. 205 */ 206 public Builder useQuotelessKeys(boolean value) { 207 useQuotelessKeys = value; 208 return this; 209 } 210 211 /** 212 * Omits root-level object braces <c>{}</c> in output. 213 * 214 * @param value <jk>true</jk> to omit root braces. 215 * @return This object. 216 */ 217 public Builder omitRootBraces(boolean value) { 218 omitRootBraces = value; 219 return this; 220 } 221 222 /** 223 * Uses newlines instead of commas between object members. 224 * 225 * @param value <jk>true</jk> for newline-separated members. 226 * @return This object. 227 */ 228 public Builder useNewlineSeparators(boolean value) { 229 useNewlineSeparators = value; 230 return this; 231 } 232 233 /** 234 * Creates compact output (comma-separated, single line). 235 * 236 * @return This object. 237 */ 238 public Builder compact() { 239 useNewlineSeparators = false; 240 return this; 241 } 242 243 @Override 244 public Builder useWhitespace() { 245 super.useWhitespace(); 246 return this; 247 } 248 249 @Override 250 public Builder useWhitespace(boolean value) { 251 super.useWhitespace(value); 252 return this; 253 } 254 255 @Override /* Overridden from WriterSerializer.Builder<?> */ 256 public Builder ws() { 257 return useWhitespace(); 258 } 259 260 @Override 261 public HjsonSerializer build() { 262 return cache(CACHE).build(HjsonSerializer.class); 263 } 264 265 @Override 266 public Builder copy() { 267 return new Builder(this); 268 } 269 270 @Override 271 public HashKey hashKey() { 272 return HashKey.of(super.hashKey(), useMultilineStrings, useQuotelessStrings, useQuotelessKeys, omitRootBraces, useNewlineSeparators); 273 } 274 } 275 276 /** Default serializer (readable). */ 277 public static final HjsonSerializer DEFAULT = new HjsonSerializer(create().ws()); 278 279 /** Compact serializer (single line). */ 280 public static final HjsonSerializer DEFAULT_COMPACT = new HjsonSerializer(create().useWhitespace(false).compact()); 281 282 /** 283 * Creates a new builder. 284 * 285 * @return A new builder. 286 */ 287 public static Builder create() { 288 return new Builder(); 289 } 290 291 /** 292 * Constructor. 293 * 294 * @param builder The builder. 295 */ 296 public HjsonSerializer(Builder builder) { 297 super(builder); 298 useMultilineStrings = builder.useMultilineStrings; 299 useQuotelessStrings = builder.useQuotelessStrings; 300 useQuotelessKeys = builder.useQuotelessKeys; 301 omitRootBraces = builder.omitRootBraces; 302 useNewlineSeparators = builder.useNewlineSeparators; 303 } 304 305 @Override 306 public HjsonSerializerSession.Builder createSession() { 307 return HjsonSerializerSession.create(this); 308 } 309 310 @Override 311 public Builder copy() { 312 return new Builder(this); 313 } 314 315 @Override 316 public HjsonBeanPropertyMeta getHjsonBeanPropertyMeta(BeanPropertyMeta bpm) { 317 if (bpm == null) 318 return HjsonBeanPropertyMeta.DEFAULT; 319 return hjsonBeanPropertyMetas.computeIfAbsent(bpm, k -> new HjsonBeanPropertyMeta(k, this)); 320 } 321 322 @Override 323 public HjsonClassMeta getHjsonClassMeta(ClassMeta<?> cm) { 324 return hjsonClassMetas.computeIfAbsent(cm, k -> new HjsonClassMeta(k, this)); 325 } 326 327 @Override 328 protected FluentMap<String,Object> properties() { 329 return super.properties() 330 .a("useMultilineStrings", useMultilineStrings) 331 .a("useQuotelessStrings", useQuotelessStrings) 332 .a("useQuotelessKeys", useQuotelessKeys) 333 .a("omitRootBraces", omitRootBraces) 334 .a("useNewlineSeparators", useNewlineSeparators); 335 } 336 337 /** 338 * Convenience delegator that opens a {@link RecordWriter} over the output using 339 * <b>default session arguments</b> (mirrors {@link #write(Object)}). 340 * 341 * <p> 342 * The real implementation lives on {@link HjsonSerializerSession#writeRecords(Object)}. 343 * Callers that need request-derived configuration should call {@link #createSession()} and 344 * invoke {@link HjsonSerializerSession#writeRecords(Object)} on the built session instead. 345 * 346 * @param output The output. 347 * @return A new {@link RecordWriter}. 348 * @throws IOException If a problem occurred opening the underlying output. 349 */ 350 @Override /* RecordWritable */ 351 public RecordWriter writeRecords(Object output) throws IOException { 352 return ((RecordWritable) getSession()).writeRecords(output); 353 } 354 355 @Override /* RecordWritable */ 356 public boolean isRecordStreaming() { 357 return false; 358 } 359}