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.IoUtils.*;
020import static org.apache.juneau.commons.utils.Shorts.*;
021
022import java.io.*;
023import java.util.*;
024import java.util.function.*;
025
026import org.apache.juneau.commons.bean.*;
027import org.apache.juneau.commons.lang.*;
028import org.apache.juneau.marshall.*;
029import org.apache.juneau.marshall.serializer.*;
030import org.apache.juneau.marshall.stream.*;
031
032/**
033 * Session object that lives for the duration of a single use of {@link YamlSerializer}.
034 *
035 * <h5 class='section'>Notes:</h5><ul>
036 *    <li class='warn'>This class is not thread safe and is typically discarded after one use.
037 * </ul>
038 *
039 * <h5 class='section'>See Also:</h5><ul>
040 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/YamlSupport">YAML</a>
041 * </ul>
042 */
043@SuppressWarnings({
044   "java:S110", // Inheritance depth acceptable for this class hierarchy
045   "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants
046   "java:S3776", // Cognitive complexity acceptable for YAML serializer session methods
047   "rawtypes", // writeMap()/writeCollection() take raw Map/Collection and writeAnything() casts to raw BeanMap/Map/Collection after ClassMeta type checks
048   "resource", // Resource management handled externally
049   "unchecked" // Raw-to-generic casts in writeAnything()/writeMap()/writeCollection() are guarded by the ClassMeta isMap()/isCollection() checks
050})
051public class YamlSerializerSession extends WriterSerializerSession implements RecordWritable, ArrayRecordWritable {
052
053   /**
054    * Builder class.
055    */
056   public static class Builder extends WriterSerializerSession.Builder<Builder> {
057
058      /**
059       * Constructor
060       *
061       * @param ctx The context creating this session.
062       *    <br>Cannot be <jk>null</jk>.
063       */
064      protected Builder(YamlSerializer ctx) {
065         super(reqnn("ctx", ctx));
066      }
067
068      @Override
069      public YamlSerializerSession build() {
070         return new YamlSerializerSession(this);
071      }
072
073   }
074
075   /**
076    * Creates a new builder for this object.
077    *
078    * @param ctx The context creating this session.
079    *    <br>Cannot be <jk>null</jk>.
080    * @return A new builder.
081    */
082   public static Builder create(YamlSerializer ctx) {
083      return new Builder(reqnn("ctx", ctx));
084   }
085
086   /**
087    * Constructor.
088    *
089    * @param builder The builder for this object.
090    */
091   protected YamlSerializerSession(Builder builder) {
092      super(builder);
093   }
094
095   @Override /* Overridden from SerializerSession */
096   protected void doWrite(SerializerPipe out, Object o) throws IOException, SerializeException {
097      writeAnything(getYamlWriter(out), o, getExpectedRootType(o), "root", null, true);
098   }
099
100   /**
101    * Opens a whole-value push generator targeting YAML output, bound to this live session.
102    * {@link RecordWriter#write(Object) write(Object)} delegates to
103    * {@link SerializerSession#write(Object, Object)}.
104    *
105    * @param output The output.
106    * @return A new {@link RecordWriter}.
107    * @throws IOException If a problem occurred opening the underlying output.
108    */
109   @Override /* RecordWritable */
110   public RecordWriter writeRecords(Object output) throws IOException {
111      return RecordAdapter.writer(this, output);
112   }
113
114   /**
115    * Buffered array-element {@link RecordWriter} for YAML, bound to this live session.  Buffers
116    * each {@code write(...)} and emits the whole list on {@code close()}.
117    *
118    * @param output The output.
119    * @return A new {@link RecordWriter}.
120    * @throws IOException If a problem occurred opening the underlying output.
121    */
122   @Override /* ArrayRecordWritable */
123   public RecordWriter writeArrayRecords(Object output) throws IOException {
124      return RecordAdapter.arrayWriter(this, output);
125   }
126
127   /**
128    * The YAML record writer is buffered/{@link RecordAdapter}-backed, not O(1) streaming.
129    *
130    * @return Always <jk>false</jk>.
131    */
132   @Override /* RecordWritable */
133   public boolean isRecordStreaming() { return false; }
134
135   /**
136    * The YAML array-record writer is buffered/{@link RecordAdapter}-backed, not O(1) streaming.
137    *
138    * @return Always <jk>false</jk>.
139    */
140   @Override /* ArrayRecordWritable */
141   public boolean isArrayRecordStreaming() { return false; }
142
143   /**
144    * Converts the specified output target object to a {@link YamlWriter}.
145    *
146    * @param out The output target object.
147    *    <br>Must not be <jk>null</jk>.
148    * @return The output target object wrapped in a {@link YamlWriter}.
149    */
150   protected final YamlWriter getYamlWriter(SerializerPipe out) {
151      var output = out.getRawOutput();
152      if (output instanceof YamlWriter output2)
153         return output2;
154      var w = new YamlWriter(out.getWriter(), isUseWhitespace(), getMaxIndent(), isTrimStrings(), getUriResolver());
155      out.setWriter(w);
156      return w;
157   }
158
159   /**
160    * Workhorse method.
161    * Determines the type of object, and then calls the appropriate type-specific serialization method.
162    *
163    * @param out The output writer.
164    * @param o The object to serialize.
165    * @param eType The expected type.
166    * @param attrName The attribute name.
167    * @param pMeta The bean property currently being parsed.
168    * @param asField If <jk>true</jk>, this value appears inline after a key or at root level.
169    *    If <jk>false</jk>, this value needs its own indentation as a standalone block.
170    * @return The same writer passed in.
171    * @throws SerializeException General serialization error occurred.
172    */
173   protected YamlWriter writeAnything(YamlWriter out, Object o, ClassMeta<?> eType, String attrName, BeanPropertyMeta pMeta, boolean asField) throws SerializeException {
174
175      if (o == null) {
176         out.append("null");
177         return out;
178      }
179
180      if (eType == null)
181         eType = object();
182
183      ClassMeta<?> aType;
184      ClassMeta<?> sType;
185
186      aType = push2(attrName, o, eType);
187      boolean isRecursion = aType == null;
188
189      if (aType == null) {
190         o = null;
191         aType = object();
192      }
193
194      if (isOptional(aType)) {
195         o = getOptionalValue(o);
196         eType = getOptionalType(eType);
197         aType = getClassMetaForObject(o, object());
198      }
199
200      sType = aType;
201      String typeName = getBeanTypeName(this, eType, aType, pMeta);
202
203      var swap = aType.getSwap(this);
204      if (nn(swap)) {
205         o = swap(swap, o);
206         sType = swap.getSwapClassMeta(this);
207
208         if (sType.isObject())
209            sType = getClassMetaForObject(o);
210      }
211
212      if (o == null || (sType.isChar() && ((Character)o).charValue() == 0)) {
213         out.append("null");
214      } else if (sType.isBean()) {
215         writeBeanMap(out, toBeanMap(o), typeName);
216      } else if (sType.isMap()) {
217         if (sType.isBeanMap())
218            writeBeanMap(out, (BeanMap)o, typeName);
219         else
220            writeMap(out, (Map)o, eType);
221      } else if (sType.isCollection()) {
222         writeCollection(out, (Collection)o, eType, asField);
223      } else if (sType.isArray()) {
224         writeCollection(out, toList(sType.inner(), o), eType, asField);
225      } else if (sType.isNumber() || sType.isBoolean()) {
226         out.append(o);
227      } else if (sType.isUri() || (nn(pMeta) && pMeta.isUri())) {
228         out.uriValue(o);
229      } else if (sType.isReader()) {
230         pipe((Reader)o, out, SerializerSession::handleThrown);
231      } else if (sType.isInputStream()) {
232         pipe((InputStream)o, out, SerializerSession::handleThrown);
233      } else {
234         out.textValue(toString(o));
235      }
236
237      if (! isRecursion)
238         pop();
239      return out;
240   }
241
242   /**
243    * Determines whether a ClassMeta represents a complex type that requires block-style YAML formatting.
244    */
245   private boolean isComplexValue(ClassMeta<?> cm) {
246      if (cm == null)
247         return false;
248      if (cm.isMapOrBean() || cm.isCollectionOrArrayOrOptional())
249         return true;
250      var swap = cm.getSwap(this);
251      if (swap != null) {
252         var swapType = swap.getSwapClassMeta(this);
253         return swapType.isMapOrBean() || swapType.isCollectionOrArrayOrOptional();
254      }
255      return false;
256   }
257
258   private SerializerWriter<?> writeBeanMap(YamlWriter out, BeanMap<?> m, String typeName) throws SerializeException {
259      int i = indent - 1;
260
261      var addAtt = Flag.create();
262      Predicate<Object> checkNull = x -> isKeepNullProperties() || nn(x);
263
264      if (nn(typeName)) {
265         BeanPropertyMeta pm = m.getMeta().getTypeProperty();
266         out.key(pm.getName()).textValue(typeName);
267         addAtt.set();
268      }
269
270      m.forEachValue(checkNull, (pMeta, key, value, thrown) -> {
271         var cMeta = (ClassMeta<?>) pMeta.getBeanInfo();
272         if (nn(thrown))
273            onBeanGetterException(pMeta, thrown);
274
275         if (canIgnoreValue(pMeta, key, value))
276            return;
277
278         addAtt.ifSet(() -> out.yamlIndent(i)).set();
279
280         ClassMeta<?> actualType = value == null ? cMeta : getClassMetaForObject(value, cMeta);
281         if (value != null && isComplexValue(actualType)) {
282            out.keyName(key).w(": ");
283            if (actualType.isCollectionOrArrayOrOptional()) {
284               writeAnything(out, value, cMeta, key, pMeta, false);
285            } else {
286               out.yamlIndent(i + 1);
287               writeAnything(out, value, cMeta, key, pMeta, true);
288            }
289         } else {
290            out.key(key);
291            writeAnything(out, value, cMeta, key, pMeta, true);
292         }
293      });
294
295      if (addAtt.isUnset())
296         out.w("{}");
297
298      return out;
299   }
300
301   private SerializerWriter<?> writeMap(YamlWriter out, Map m, ClassMeta<?> type) throws SerializeException {
302      int i = indent - 1;
303
304      var keyType = type.getKeyType();
305      var valueType = type.getValueType();
306
307      if (m.isEmpty()) {
308         out.w("{}");
309         return out;
310      }
311
312      var addAtt = Flag.create();
313      forEachEntry(m, x -> {
314         addAtt.ifSet(() -> out.yamlIndent(i)).set();
315         Object value = x.getValue();
316         Object key = generalize(x.getKey(), keyType);
317
318         ClassMeta<?> actualType = value == null ? valueType : getClassMetaForObject(value, valueType);
319         if (value != null && isComplexValue(actualType)) {
320            out.keyName(toString(key)).w(": ");
321            if (actualType.isCollectionOrArrayOrOptional()) {
322               writeAnything(out, value, valueType, (key == null ? null : toString(key)), null, false);
323            } else {
324               out.yamlIndent(i + 1);
325               writeAnything(out, value, valueType, (key == null ? null : toString(key)), null, true);
326            }
327         } else {
328            out.key(toString(key));
329            writeAnything(out, value, valueType, (key == null ? null : toString(key)), null, true);
330         }
331      });
332
333      return out;
334   }
335
336   private SerializerWriter<?> writeCollection(YamlWriter out, Collection c, ClassMeta<?> type, boolean asField) throws SerializeException {
337      int i = indent - 1;
338
339      if (c.isEmpty()) {
340         out.w("[]");
341         return out;
342      }
343
344      var elementType = type.getElementType();
345
346      var addEntry = Flag.create();
347      forEachEntry(c, x -> {
348         if (asField && addEntry.isUnset()) {
349            out.w("- ");
350         } else {
351            out.listEntry(i);
352         }
353         addEntry.set();
354         writeAnything(out, x, elementType, "<iterator>", null, true);
355      });
356
357      return out;
358   }
359
360}