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}