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.*;
020
021import java.io.*;
022
023import org.apache.juneau.commons.bean.*;
024import org.apache.juneau.commons.collections.*;
025import org.apache.juneau.marshall.*;
026import org.apache.juneau.marshall.json.*;
027import org.apache.juneau.marshall.parser.*;
028import org.apache.juneau.marshall.stream.*;
029
030/**
031 * Parses YAML text into a POJO model.
032 *
033 * <h5 class='topic'>Media types</h5>
034 * <p>
035 * Handles <c>Content-Type</c> types:  <bc>application/yaml, text/yaml</bc>
036 *
037 * <h5 class='topic'>Description</h5>
038 * <p>
039 * This parser uses an indentation-aware state machine to parse YAML directly into POJOs without intermediate
040 * DOM objects.  It supports block-style and flow-style mappings and sequences, plain and quoted scalars,
041 * comments, document markers, and standard YAML scalar types.
042 *
043 * <h5 class='section'>Limitations compared to JSON</h5>
044 * <p>
045 * The YAML parser has some limitations when compared to {@link JsonParser}:
046 * <ul class='spaced-list'>
047 *    <li>
048 *       Maps with non-String keys ({@link Boolean}, {@link java.util.Date}, {@link java.time.temporal.Temporal},
049 *       {@link Enum}) may not round-trip correctly when the target type is a generic {@link java.util.Map} with
050 *       those key types.  {@link java.util.LinkedHashMap LinkedHashMap} and {@link java.util.TreeMap TreeMap}
051 *       with {@link String} keys work reliably.
052 *    <li>
053 *       No strict vs non-strict mode; unlike JSON, there is no equivalent to JSON's lax parsing of comments,
054 *       unquoted attributes, or concatenated strings.
055 *    <li>
056 *       YAML's indentation-based structure requires consistent formatting; malformed indentation can cause
057 *       parsing errors where equivalent JSON would parse successfully.
058 * </ul>
059 *
060 * <h5 class='figure'>Example input (Map of name/age):</h5>
061 * <p class='bcode'>
062 *    name: Alice
063 *    age: 30
064 * </p>
065 *
066 * <h5 class='figure'>Complex (nested object + array):</h5>
067 * <p class='bcode'>
068 *    name: Alice
069 *    age: 30
070 *    address:
071 *      street: 123 Main St
072 *      city: Boston
073 *      state: MA
074 *    tags:
075 *    - a
076 *    - b
077 *    - c
078 * </p>
079 *
080 * <h5 class='section'>Notes:</h5><ul>
081 *    <li class='note'>This class is thread safe and reusable.
082 * </ul>
083 *
084 * <h5 class='section'>See Also:</h5><ul>
085 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/YamlSupport">YAML</a>
086 * </ul>
087 */
088@SuppressWarnings({
089   "java:S110", // Inheritance depth acceptable
090   "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants
091   "java:S9149", // Per-format static factories intentionally shadow the parent's.
092   "resource" // Closeable resources are owned by the caller's parser session; Eclipse JDT @Owning warning is by design.
093})
094public class YamlParser extends ReaderParser implements YamlMetaProvider, RecordReadable, ArrayRecordReadable {
095
096   private final java.util.concurrent.ConcurrentHashMap<ClassMeta<?>,YamlClassMeta> yamlClassMetas = new java.util.concurrent.ConcurrentHashMap<>();
097   private final java.util.concurrent.ConcurrentHashMap<BeanPropertyMeta,YamlBeanPropertyMeta> yamlBeanPropertyMetas = new java.util.concurrent.ConcurrentHashMap<>();
098
099   /**
100    * Builder class.
101    */
102   public static class Builder extends ReaderParser.Builder<Builder> {
103
104      private static final Cache<HashKey,YamlParser> CACHE = Cache.of(HashKey.class, YamlParser.class).build();
105
106      /**
107       * Constructor, default settings.
108       */
109      protected Builder() {
110         consumes("application/yaml,text/yaml");
111      }
112
113      /**
114       * Copy constructor.
115       *
116       * @param copyFrom The builder to copy from.
117       *    <br>Cannot be <jk>null</jk>.
118       */
119      protected Builder(Builder copyFrom) {
120         super(reqnn("copyFrom", copyFrom));
121      }
122
123      /**
124       * Copy constructor.
125       *
126       * @param copyFrom The bean to copy from.
127       *    <br>Cannot be <jk>null</jk>.
128       */
129      protected Builder(YamlParser copyFrom) {
130         super(reqnn("copyFrom", copyFrom));
131      }
132
133      @Override /* Overridden from Context.Builder<?> */
134      public YamlParser build() {
135         return cache(CACHE).build(YamlParser.class);
136      }
137
138      @Override /* Overridden from Context.Builder<?> */
139      public Builder copy() {
140         return new Builder(this);
141      }
142
143      @Override /* Overridden from Context.Builder<?> */
144      public HashKey hashKey() {
145         return HashKey.of(super.hashKey());
146      }
147
148
149   }
150
151   /** Default parser, all default settings.*/
152   public static final YamlParser DEFAULT = new YamlParser(create());
153
154   /**
155    * Creates a new builder for this object.
156    *
157    * @return A new builder.
158    */
159   public static Builder create() {
160      return new Builder();
161   }
162
163   /**
164    * Constructor.
165    *
166    * @param builder The builder for this object.
167    */
168   public YamlParser(Builder builder) {
169      super(builder);
170   }
171
172   @Override /* Overridden from YamlMetaProvider */
173   public YamlBeanPropertyMeta getYamlBeanPropertyMeta(BeanPropertyMeta bpm) {
174      if (bpm == null)
175         return YamlBeanPropertyMeta.DEFAULT;
176      return yamlBeanPropertyMetas.computeIfAbsent(bpm, k -> new YamlBeanPropertyMeta(k, this));
177   }
178
179   @Override /* Overridden from YamlMetaProvider */
180   public YamlClassMeta getYamlClassMeta(ClassMeta<?> cm) {
181      return yamlClassMetas.computeIfAbsent(cm, k -> new YamlClassMeta(k, this));
182   }
183
184   @Override /* Overridden from Context */
185   public Builder copy() {
186      return new Builder(this);
187   }
188
189   @Override /* Overridden from Context */
190   public YamlParserSession.Builder createSession() {
191      return YamlParserSession.create(this);
192   }
193
194   @Override /* Overridden from Context */
195   public YamlParserSession getSession() { return createSession().build(); }
196
197   /**
198    * Convenience delegator for the whole-value {@link RecordReader} using <b>default session
199    * arguments</b>.  The real implementation lives on {@link YamlParserSession#readRecords(Object)}.
200    *
201    * @param input The input.
202    * @return A new {@link RecordReader} cursor.
203    * @throws IOException If a problem occurred opening the underlying input.
204    */
205   @Override /* RecordReadable */
206   public RecordReader readRecords(Object input) throws IOException {
207      return getSession().readRecords(input);
208   }
209
210   /**
211    * Convenience delegator for the buffered array-element {@link RecordReader} using <b>default
212    * session arguments</b>.  The real implementation lives on
213    * {@link YamlParserSession#readArrayRecords(Object)}.
214    *
215    * @param input The input.
216    * @return A buffered {@link RecordReader}.
217    * @throws IOException If a problem occurred reading the input.
218    */
219   @Override /* ArrayRecordReadable */
220   public RecordReader readArrayRecords(Object input) throws IOException {
221      return getSession().readArrayRecords(input);
222   }
223
224   /**
225    * The YAML record cursor is buffered/{@link RecordAdapter}-backed, not O(1) streaming.
226    *
227    * @return Always <jk>false</jk>.
228    */
229   @Override /* RecordReadable */
230   public boolean isRecordStreaming() { return false; }
231
232   /**
233    * The YAML array-record cursor is buffered/{@link RecordAdapter}-backed, not O(1) streaming.
234    *
235    * @return Always <jk>false</jk>.
236    */
237   @Override /* ArrayRecordReadable */
238   public boolean isArrayRecordStreaming() { return false; }
239}