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.yaml; 018 019import static org.apache.juneau.commons.utils.Shorts.*; 020import static org.apache.juneau.commons.utils.SystemUtils.*; 021 022import java.io.*; 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.json.*; 029import org.apache.juneau.marshall.serializer.*; 030import org.apache.juneau.marshall.stream.*; 031 032/** 033 * Serializes POJO models to YAML. 034 * 035 * <h5 class='topic'>Media types</h5> 036 * <p> 037 * Handles <c>Accept</c> types: <bc>application/yaml, text/yaml</bc> 038 * <p> 039 * Produces <c>Content-Type</c> types: <bc>application/yaml</bc> 040 * 041 * <h5 class='topic'>Description</h5> 042 * <p> 043 * The conversion is as follows... 044 * <ul class='spaced-list'> 045 * <li> 046 * Maps (e.g. {@link HashMap HashMaps}, {@link TreeMap TreeMaps}) are converted to YAML mappings. 047 * <li> 048 * Collections (e.g. {@link HashSet HashSets}, {@link LinkedList LinkedLists}) and Java arrays are converted to 049 * YAML sequences. 050 * <li> 051 * {@link String Strings} are converted to YAML scalars. 052 * <li> 053 * {@link Number Numbers} (e.g. {@link Integer}, {@link Long}, {@link Double}) are converted to YAML numbers. 054 * <li> 055 * {@link Boolean Booleans} are converted to YAML booleans. 056 * <li> 057 * {@code nulls} are converted to YAML nulls. 058 * <li> 059 * {@code arrays} are converted to YAML sequences. 060 * <li> 061 * {@code beans} are converted to YAML mappings. 062 * </ul> 063 * 064 * <p> 065 * The types above are considered "YAML-primitive" object types. 066 * Any non-YAML-primitive object types are transformed into YAML-primitive object types through 067 * {@link org.apache.juneau.marshall.swap.spi.ObjectSwap ObjectSwaps} associated through the 068 * {@link org.apache.juneau.marshall.MarshallingContext.Builder#swaps(Class...) MarshallingContext.Builder#swaps(Class...)} method. 069 * Several default transforms are provided for transforming Dates, Enums, Iterators, etc... 070 * 071 * <p> 072 * This serializer provides several serialization options. 073 * Typically, one of the predefined DEFAULT serializers will be sufficient. 074 * However, custom serializers can be constructed to fine-tune behavior. 075 * 076 * <h5 class='section'>Example:</h5> 077 * <p class='bjava'> 078 * <jc>// Use one of the default serializers to serialize a POJO</jc> 079 * String <jv>yaml</jv> = YamlSerializer.<jsf>DEFAULT</jsf>.write(<jv>someObject</jv>); 080 * 081 * <jc>// Create a custom serializer</jc> 082 * YamlSerializer <jv>serializer</jv> = YamlSerializer.<jsm>create</jsm>().build(); 083 * 084 * <jc>// Clone an existing serializer and modify it to use whitespace</jc> 085 * <jv>serializer</jv> = YamlSerializer.<jsf>DEFAULT</jsf>.copy().ws().build(); 086 * 087 * <jc>// Serialize a POJO to YAML</jc> 088 * <jv>yaml</jv> = <jv>serializer</jv>.write(<jv>someObject</jv>); 089 * </p> 090 * 091 * <h5 class='figure'>Example output (LinkedHashMap of name/age):</h5> 092 * <p class='bcode'> 093 * name: Alice 094 * age: 30 095 * </p> 096 * 097 * <h5 class='figure'>Complex (nested bean + array; bean properties are sorted alphabetically by default):</h5> 098 * <p class='bcode'> 099 * address: 100 * city: Boston 101 * state: MA 102 * street: "123 Main St" 103 * age: 30 104 * name: Alice 105 * tags: 106 * - a 107 * - b 108 * - c 109 * </p> 110 * 111 * <h5 class='section'>Limitations compared to JSON</h5> 112 * <p> 113 * The YAML serializer has fewer configuration options than {@link JsonSerializer}: 114 * <ul class='spaced-list'> 115 * <li> 116 * No compact single-line output mode; YAML is always emitted in block-style (indentation-based) format. 117 * <li> 118 * No equivalent to JSON's simple mode or attribute quoting style variants (single vs double quotes). 119 * <li> 120 * No strict vs lax output modes; YAML output follows a consistent, human-readable style. 121 * </ul> 122 * 123 * <h5 class='section'>Notes:</h5><ul> 124 * <li class='note'>This class is thread safe and reusable. 125 * </ul> 126 * 127 * <h5 class='section'>See Also:</h5><ul> 128 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/YamlSupport">YAML</a> 129 * </ul> 130 */ 131@SuppressWarnings({ 132 "java:S110", // Inheritance depth acceptable for this class hierarchy 133 "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants 134 "java:S9149", // Per-format static factories intentionally shadow the parent's. 135 "resource" // Closeable resources are owned by the caller's serializer session; Eclipse JDT @Owning warning is by design. 136}) 137public class YamlSerializer extends WriterSerializer implements YamlMetaProvider, RecordWritable, ArrayRecordWritable { 138 139 private final java.util.concurrent.ConcurrentHashMap<ClassMeta<?>,YamlClassMeta> yamlClassMetas = new java.util.concurrent.ConcurrentHashMap<>(); 140 private final java.util.concurrent.ConcurrentHashMap<BeanPropertyMeta,YamlBeanPropertyMeta> yamlBeanPropertyMetas = new java.util.concurrent.ConcurrentHashMap<>(); 141 142 /** 143 * Builder class. 144 */ 145 public static class Builder extends WriterSerializer.Builder<Builder> { 146 147 private static final Cache<HashKey,YamlSerializer> CACHE = Cache.of(HashKey.class, YamlSerializer.class).build(); 148 149 private boolean addBeanTypesYaml; 150 151 /** 152 * Constructor, default settings. 153 */ 154 protected Builder() { 155 produces("application/yaml"); 156 accept("application/yaml,text/yaml"); 157 addBeanTypesYaml = env("YamlSerializer.addBeanTypes", false); 158 } 159 160 /** 161 * Copy constructor. 162 * 163 * @param copyFrom The builder to copy from. 164 * <br>Cannot be <jk>null</jk>. 165 */ 166 protected Builder(Builder copyFrom) { 167 super(reqnn("copyFrom", copyFrom)); 168 addBeanTypesYaml = copyFrom.addBeanTypesYaml; 169 } 170 171 /** 172 * Copy constructor. 173 * 174 * @param copyFrom The bean to copy from. 175 * <br>Cannot be <jk>null</jk>. 176 */ 177 protected Builder(YamlSerializer copyFrom) { 178 super(reqnn("copyFrom", copyFrom)); 179 addBeanTypesYaml = copyFrom.addBeanTypesYaml; 180 } 181 182 /** 183 * Add <js>"_type"</js> properties when needed. 184 * 185 * <p> 186 * If <jk>true</jk>, then <js>"_type"</js> properties will be added to beans if their type cannot be inferred 187 * through reflection. 188 * 189 * <p> 190 * When present, this value overrides the {@link org.apache.juneau.marshall.serializer.Serializer.Builder#addBeanTypes() Serializer.Builder#addBeanTypes()} setting and is 191 * provided to customize the behavior of specific serializers in a {@link SerializerSet}. 192 * 193 * @return This object. 194 */ 195 public Builder addBeanTypesYaml() { 196 return addBeanTypesYaml(true); 197 } 198 199 /** 200 * Same as {@link #addBeanTypesYaml()} but allows you to explicitly specify the value. 201 * 202 * @param value The value for this setting. 203 * @return This object. 204 */ 205 public Builder addBeanTypesYaml(boolean value) { 206 addBeanTypesYaml = value; 207 return this; 208 } 209 210 @Override /* Overridden from Context.Builder<?> */ 211 public YamlSerializer build() { 212 return cache(CACHE).build(YamlSerializer.class); 213 } 214 215 @Override /* Overridden from Context.Builder<?> */ 216 public Builder copy() { 217 return new Builder(this); 218 } 219 220 @Override /* Overridden from Context.Builder<?> */ 221 public HashKey hashKey() { 222 return HashKey.of( 223 super.hashKey(), 224 addBeanTypesYaml 225 ); 226 } 227 228 229 } 230 231 /** Default serializer, with whitespace. */ 232 public static class Readable extends YamlSerializer { 233 234 /** 235 * Constructor. 236 * 237 * @param builder The builder for this object. 238 */ 239 public Readable(Builder builder) { 240 super(builder.useWhitespace()); 241 } 242 } 243 244 /** Default serializer, all default settings. */ 245 public static final YamlSerializer DEFAULT = new YamlSerializer(create()); 246 247 /** Default serializer, with whitespace. */ 248 public static final YamlSerializer DEFAULT_READABLE = new Readable(create()); 249 250 /** 251 * Creates a new builder for this object. 252 * 253 * @return A new builder. 254 */ 255 public static Builder create() { 256 return new Builder(); 257 } 258 259 protected final boolean addBeanTypesYaml; 260 261 private final boolean addBeanTypes2; 262 263 /** 264 * Constructor. 265 * 266 * @param builder The builder for this object. 267 */ 268 public YamlSerializer(Builder builder) { 269 super(builder); 270 addBeanTypesYaml = builder.addBeanTypesYaml; 271 addBeanTypes2 = addBeanTypesYaml || super.isAddBeanTypes(); 272 } 273 274 @Override /* Overridden from YamlMetaProvider */ 275 public YamlBeanPropertyMeta getYamlBeanPropertyMeta(BeanPropertyMeta bpm) { 276 if (bpm == null) 277 return YamlBeanPropertyMeta.DEFAULT; 278 return yamlBeanPropertyMetas.computeIfAbsent(bpm, k -> new YamlBeanPropertyMeta(k, this)); 279 } 280 281 @Override /* Overridden from YamlMetaProvider */ 282 public YamlClassMeta getYamlClassMeta(ClassMeta<?> cm) { 283 return yamlClassMetas.computeIfAbsent(cm, k -> new YamlClassMeta(k, this)); 284 } 285 286 @Override /* Overridden from Context */ 287 public Builder copy() { 288 return new Builder(this); 289 } 290 291 @Override /* Overridden from Context */ 292 public YamlSerializerSession.Builder createSession() { 293 return YamlSerializerSession.create(this); 294 } 295 296 @Override /* Overridden from Context */ 297 public YamlSerializerSession getSession() { return createSession().build(); } 298 299 /** 300 * Add <js>"_type"</js> properties when needed. 301 * 302 * @see Builder#addBeanTypesYaml() 303 * @return 304 * <jk>true</jk> if <js>"_type"</js> properties will be added to beans if their type cannot be inferred 305 * through reflection. 306 */ 307 @Override 308 protected final boolean isAddBeanTypes() { return addBeanTypes2; } 309 310 @Override /* Overridden from WriterSerializer */ 311 protected FluentMap<String,Object> properties() { 312 return super.properties() 313 .a("addBeanTypesYaml", addBeanTypesYaml); 314 } 315 316 /** 317 * Convenience delegator for the whole-value {@link RecordWriter} using <b>default session 318 * arguments</b>. The real implementation lives on 319 * {@link YamlSerializerSession#writeRecords(Object)}. 320 * 321 * @param output The output. 322 * @return A new {@link RecordWriter}. 323 * @throws IOException If a problem occurred opening the underlying output. 324 */ 325 @Override /* RecordWritable */ 326 public RecordWriter writeRecords(Object output) throws IOException { 327 return getSession().writeRecords(output); 328 } 329 330 /** 331 * Convenience delegator for the buffered array-element {@link RecordWriter} using <b>default 332 * session arguments</b>. The real implementation lives on 333 * {@link YamlSerializerSession#writeArrayRecords(Object)}. 334 * 335 * @param output The output. 336 * @return A new {@link RecordWriter}. 337 * @throws IOException If a problem occurred opening the underlying output. 338 */ 339 @Override /* ArrayRecordWritable */ 340 public RecordWriter writeArrayRecords(Object output) throws IOException { 341 return getSession().writeArrayRecords(output); 342 } 343 344 /** 345 * The YAML record writer is buffered/{@link RecordAdapter}-backed, not O(1) streaming. 346 * 347 * @return Always <jk>false</jk>. 348 */ 349 @Override /* RecordWritable */ 350 public boolean isRecordStreaming() { return false; } 351 352 /** 353 * The YAML array-record writer is buffered/{@link RecordAdapter}-backed, not O(1) streaming. 354 * 355 * @return Always <jk>false</jk>. 356 */ 357 @Override /* ArrayRecordWritable */ 358 public boolean isArrayRecordStreaming() { return false; } 359}