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.lang.StateEnum.*;
020import static org.apache.juneau.commons.utils.CollectionUtils.*;
021import static org.apache.juneau.commons.utils.Shorts.*;
022import static org.apache.juneau.commons.utils.StringUtils.*;
023
024import java.io.*;
025import java.lang.reflect.*;
026import java.util.*;
027
028import org.apache.juneau.commons.bean.*;
029import org.apache.juneau.commons.reflect.*;
030import org.apache.juneau.commons.utils.*;
031import org.apache.juneau.marshall.*;
032import org.apache.juneau.marshall.collections.*;
033import org.apache.juneau.marshall.parser.*;
034import org.apache.juneau.marshall.stream.*;
035import org.apache.juneau.marshall.swap.spi.*;
036
037/**
038 * Session object that lives for the duration of a single use of {@link YamlParser}.
039 *
040 * <h5 class='section'>Notes:</h5><ul>
041 *    <li class='warn'>This class is not thread safe and is typically discarded after one use.
042 * </ul>
043 */
044@SuppressWarnings({
045   "java:S107", // 8 parameters required for YAML scalar handling dispatch
046   "java:S125", // State-machine and parse-path comments are documentation, not commented-out code
047   "java:S135", // Multiple break/continue acceptable for YAML parsing state machines
048   "java:S1168", // handlePlainScalar(), readFlowMapping(), readFlowSequence() and readBlockMappingKey() return null for an empty scalar, null key or the unreachable fall-through, which callers treat as "no value"
049   "java:S1172", // eType, pMeta kept for API consistency with callers
050   "java:S2583", // Conditions look constant to static analysis but depend on parser state mutated by called helpers.
051   "java:S2589", // State checks in error path - analyzer FP on state machine flow
052   "java:S2677", // r.read() return value intentionally ignored when consuming/skipping chars
053   "java:S3626", // Redundant jump acceptable for state machine clarity
054   "java:S3776", // readAnything(), readBlockScalar(), readFlowMapping() and readIntoBeanMapFlow() are branch-heavy dispatches over YAML syntax forms; splitting them would obscure the grammar
055   "java:S6541", // readAnything() and readBlockScalar() are long single-pass dispatches over the YAML scalar/flow/block forms
056   "rawtypes", // Raw Map/Collection/ArrayList are used in readAnything(), handleQuotedScalar() and handlePlainScalar() when instantiating the reflectively resolved target type
057   "resource", // Closeable resources are owned by the caller's parser session; Eclipse JDT @Owning warning is by design.
058   "unchecked", // Casts to T/E and Class<? extends Number> in readAnything(), readFlowSequence(), readBlockSequence() and convertToType() are guarded by the ClassMeta checks on the requested type
059   "unused" // eType/pMeta/keyIndent on the private handleQuotedScalar(), handlePlainScalar() and convertToType() helpers are kept so they share readAnything()'s dispatch signature
060})
061public class YamlParserSession extends ReaderParserSession implements RecordReadable, ArrayRecordReadable {
062
063   /**
064    * Builder class.
065    */
066   public static class Builder extends ReaderParserSession.Builder<Builder> {
067
068      private final YamlParser ctx;
069
070      /**
071       * Constructor
072       *
073       * @param ctx The context creating this session.
074       */
075      protected Builder(YamlParser ctx) {
076         super(ctx);
077         this.ctx = ctx;
078      }
079
080      @Override
081      public YamlParserSession build() {
082         return new YamlParserSession(this);
083      }
084
085   }
086
087   /**
088    * Creates a new builder for this object.
089    *
090    * @param ctx The context creating this session.
091    *    <br>Cannot be <jk>null</jk>.
092    * @return A new builder.
093    */
094   public static Builder create(YamlParser ctx) {
095      return new Builder(ctx);
096   }
097
098   private final YamlParser ctx;
099
100   /**
101    * Constructor.
102    *
103    * @param builder The builder for this object.
104    */
105   protected YamlParserSession(Builder builder) {
106      super(builder);
107      ctx = builder.ctx;
108   }
109
110   @Override /* Overridden from ParserSession */
111   protected <T> T doRead(ParserPipe pipe, ClassMeta<T> type) throws IOException, ParseException, ExecutableException {
112      try (var r = pipe.getParserReader()) {
113         if (r == null)
114            return null;
115         return readAnything(type, r, getOuter(), null);
116      }
117   }
118
119   /**
120    * Opens a whole-value pull-parser cursor over a YAML document, bound to this live session.
121    * {@link RecordReader#read(Class) read(...)} delegates to the polymorphic
122    * {@link ParserSession#read(Object, Class)} entry point.
123    *
124    * @param input The input.
125    * @return A new {@link RecordReader} cursor.
126    * @throws IOException If a problem occurred opening the underlying input.
127    */
128   @Override /* RecordReadable */
129   public RecordReader readRecords(Object input) throws IOException {
130      return RecordAdapter.reader(this, input);
131   }
132
133   /**
134    * Buffered array-element {@link RecordReader} for YAML, bound to this live session.  Calls
135    * {@code parse(input, List.class, Object.class)} once and iterates the result.
136    *
137    * <h5 class='section'>Streaming status:</h5>
138    * <p>
139    * A top-level YAML block sequence ({@code - elem} per line) <i>is</i> forward-readable in
140    * principle, so true element-at-a-time streaming is <b>feasible</b> here (unlike Parquet/BSON).
141    * It is left buffered for now: Juneau's YAML reader is a single hand-written indentation state
142    * machine ({@code readBlockSequence}/{@code readFlowSequence}) whose per-element advance and
143    * dedent bookkeeping are not currently exposed as a pull cursor, and extracting that safely is a
144    * non-trivial parser refactor with real regression surface.  This
145    * is demand-driven &mdash; convert when a large-YAML-array use case justifies the refactor.
146    *
147    * @param input The input.
148    * @return A buffered {@link RecordReader}.
149    * @throws IOException If a problem occurred reading the input.
150    */
151   @Override /* ArrayRecordReadable */
152   public RecordReader readArrayRecords(Object input) throws IOException {
153      return RecordAdapter.arrayReader(this, input);
154   }
155
156   /**
157    * The YAML record cursor is buffered/{@link RecordAdapter}-backed, not O(1) streaming.
158    *
159    * @return Always <jk>false</jk>.
160    */
161   @Override /* RecordReadable */
162   public boolean isRecordStreaming() { return false; }
163
164   /**
165    * The YAML array-record cursor is buffered/{@link RecordAdapter}-backed, not O(1) streaming.
166    *
167    * <p>
168    * Genuine streaming is feasible for YAML block sequences but deferred (demand-driven) pending a
169    * pull-cursor refactor of the indentation state machine; see {@link #readArrayRecords(Object)}.
170    *
171    * @return Always <jk>false</jk>.
172    */
173   @Override /* ArrayRecordReadable */
174   public boolean isArrayRecordStreaming() { return false; }
175
176   @Override /* Overridden from ReaderParserSession */
177   protected <E> Collection<E> doReadIntoCollection(ParserPipe pipe, Collection<E> c, Type elementType) throws IOException, ParseException, ExecutableException {
178      try (var r = pipe.getParserReader()) {
179         skipWhitespaceAndComments(r);
180         int ch = r.peek();
181         if (ch == '[') {
182            readFlowSequence(r, c, getClassMeta(elementType), null);
183         } else {
184            readBlockSequence(r, c, getClassMeta(elementType), null, 0);
185         }
186         return c;
187      }
188   }
189
190   @Override /* Overridden from ReaderParserSession */
191   protected <K,V> Map<K,V> doReadIntoMap(ParserPipe pipe, Map<K,V> m, Type keyType, Type valueType) throws IOException, ParseException, ExecutableException {
192      try (var r = pipe.getParserReader()) {
193         skipWhitespaceAndComments(r);
194         int ch = r.peek();
195         if (ch == '{') {
196            readFlowMapping(r, m, (ClassMeta<K>)getClassMeta(keyType), (ClassMeta<V>)getClassMeta(valueType), null);
197         } else {
198            readBlockMapping(r, m, (ClassMeta<K>)getClassMeta(keyType), (ClassMeta<V>)getClassMeta(valueType), null, 0);
199         }
200         return m;
201      }
202   }
203
204   private <T> T readAnything(ClassMeta<?> eType, ParserReader r, Object outer, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException {
205
206      if (eType == null)
207         eType = object();
208      var swap = (ObjectSwap<T,Object>)eType.getSwap(this);
209      var builder = (BuilderSwap<T,Object>)eType.getBuilderSwap(this);
210      ClassMeta<?> sType = null;
211      if (nn(builder))
212         sType = builder.getBuilderClassMeta(this);
213      else if (nn(swap))
214         sType = swap.getSwapClassMeta(this);
215      else
216         sType = eType;
217
218      if (sType.isOptional())
219         return (T)o(readAnything(eType.getElementType(), r, outer, pMeta));
220
221      setCurrentClass(sType);
222
223      Object o = null;
224
225      skipWhitespaceAndComments(r);
226      skipDocumentMarker(r);
227      skipWhitespaceAndComments(r);
228
229      int contentColumn = r.getPosition().getColumn();
230      int c = r.peek();
231      if (c == -1) {
232         // Let o be null.
233      } else if (c == '{') {
234         if (sType.isObject()) {
235            var m2 = newGenericMap();
236            readFlowMapping(r, m2, string(), object(), pMeta);
237            o = cast(m2, pMeta, eType);
238         } else if (nn(builder)) {
239            var m = toBeanMap(builder.create(this, eType));
240            o = builder.build(this, checkRequired(readIntoBeanMap(r, m)).getBean(), eType);
241         } else if (sType.canCreateNewBean(outer)) {
242            var m = newBeanMap(outer, sType.inner());
243            o = checkRequired(readIntoBeanMap(r, m)).getBean();
244         } else if (sType.isMap()) {
245            Map m = (sType.canCreateNewInstance(outer) ? (Map)sType.newInstance(outer) : newGenericMap(sType));
246            o = readFlowMapping(r, m, sType.getKeyType(), sType.getValueType(), pMeta);
247         } else if (sType.isCollection()) {
248            var m = newGenericMap();
249            readFlowMapping(r, m, string(), object(), pMeta);
250            o = cast(m, pMeta, eType);
251         } else if (sType.isArray() || sType.isArgs()) {
252            // Unlike the isCollection() arm above, cast(m, pMeta, eType) has no way to turn a plain Map
253            // into an array -- it only resolves a "_type" bean discriminator key, which a flow mapping
254            // read this way will essentially never carry -- so it used to just hand the caller back the
255            // raw Map unchanged, silently returning the wrong runtime type for the requested array target.
256            // A flow mapping has no sequence shape to convert from, so reject it outright instead.
257            throw new ParseException(this, "Cannot read a YAML flow mapping into array type '%s'.", sType);
258         } else {
259            Map m = newGenericMap();
260            readFlowMapping(r, m, sType.getKeyType(), sType.getValueType(), pMeta);
261            if (m.containsKey(getBeanTypePropertyName(eType)))
262               o = cast((MarshalledMap)m, pMeta, eType);
263            else if (nn(sType.getProxyInvocationHandler()))
264               o = checkRequired(newBeanMap(outer, sType.inner()).load(m)).getBean();
265            else
266               throw new ParseException(this, "Class '%s' could not be instantiated.  Reason: '%s'", cn(sType), sType.getNotABeanReason());
267         }
268      } else if (c == '[') {
269         if (sType.isObject()) {
270            o = readFlowSequence(r, newGenericList(), object(), pMeta);
271         } else if (sType.isCollection()) {
272            Collection l = (sType.canCreateNewInstance(outer) ? (Collection)sType.newInstance() : newGenericList());
273            o = readFlowSequence(r, l, sType, pMeta);
274         } else if (sType.isArray() || sType.isArgs()) {
275            var l = (ArrayList)readFlowSequence(r, list(), sType, pMeta);
276            o = toArray(sType, l);
277         } else {
278            throw new ParseException(this, "Unrecognized syntax for class type '%s', starting character '%s'", sType, (char)c);
279         }
280      } else if (c == '\'') {
281         String s = readSingleQuotedString(r);
282         o = handleQuotedScalar(s, r, sType, eType, builder, outer, pMeta, contentColumn > 0 ? contentColumn - 1 : 0);
283      } else if (c == '"') {
284         String s = readDoubleQuotedString(r);
285         o = handleQuotedScalar(s, r, sType, eType, builder, outer, pMeta, contentColumn > 0 ? contentColumn - 1 : 0);
286      } else if (c == '-') {
287         int c2 = peekSecondChar(r);
288         if (c2 == ' ' || c2 == '\n' || c2 == '\r') {
289            if (sType.isObject()) {
290               o = readBlockSequence(r, newGenericList(), object(), pMeta, 0);
291            } else if (sType.isCollection()) {
292               Collection l = (sType.canCreateNewInstance(outer) ? (Collection)sType.newInstance() : newGenericList());
293               o = readBlockSequence(r, l, sType, pMeta, 0);
294            } else if (sType.isArray() || sType.isArgs()) {
295               var l = (ArrayList)readBlockSequence(r, list(), sType, pMeta, 0);
296               o = toArray(sType, l);
297            } else {
298               throw new ParseException(this, "Unrecognized syntax for class type '%s', starting character '%s'", sType, (char)c);
299            }
300         } else {
301            String s = readPlainScalar(r, 0);
302            o = convertToType(s, sType, eType, outer, pMeta);
303         }
304      } else if (c == '|' || c == '>') {
305         String s = readBlockScalar(r, (char)c);
306         o = convertToType(s, sType, eType, outer, pMeta);
307      } else if (c == '~') {
308         r.read(); // consume '~'
309         // o remains null (initialized at line 255)
310      } else {
311         String s = readPlainScalar(r, 0);
312         o = handlePlainScalar(s, r, sType, eType, builder, outer, pMeta, contentColumn > 0 ? contentColumn - 1 : 0);
313      }
314
315      if (nn(swap) && nn(o))
316         o = unswap(swap, o, eType);
317
318      if (nn(parentBean()))
319         setParent(eType, o, parentBean());
320
321      return (T)o;
322   }
323
324   private <T> Object handleQuotedScalar(String s, ParserReader r, ClassMeta<?> sType, ClassMeta<?> eType, BuilderSwap<T,Object> builder, Object outer, BeanPropertyMeta pMeta, int keyIndent) throws IOException, ParseException, ExecutableException {
325      if (looksLikeMappingKey(r)) {
326         r.read(); // consume ':'
327         int cp = r.peek();
328         if (cp == ' ')
329            r.read();
330
331         if (sType.isObject()) {
332            var m2 = newGenericMap();
333            Object value = readAnything(object(), r, m2, pMeta);
334            String ts = trim(s);
335            setName(object(), value, ts);
336            m2.put(ts, value);
337            readBlockMappingRemainder(r, m2, string(), object(), pMeta, keyIndent);
338            return cast(m2, pMeta, eType);
339         } else if (nn(builder)) {
340            var m = toBeanMap(builder.create(this, eType));
341            readBeanProperty(r, m, s);
342            readIntoBeanMapBlockRemainder(r, m, keyIndent);
343            return builder.build(this, checkRequired(m).getBean(), eType);
344         } else if (sType.canCreateNewBean(outer)) {
345            var m = newBeanMap(outer, sType.inner());
346            readBeanProperty(r, m, s);
347            readIntoBeanMapBlockRemainder(r, m, keyIndent);
348            return checkRequired(m).getBean();
349         } else if (sType.isMap()) {
350            Map m = (sType.canCreateNewInstance(outer) ? (Map)sType.newInstance(outer) : newGenericMap(sType));
351            Object value = readAnything(sType.getValueType(), r, m, pMeta);
352            Object key = convertAttrToType(m, trim(s), sType.getKeyType());
353            setName(sType.getValueType(), value, key);
354            m.put(key, value);
355            readBlockMappingRemainder(r, m, sType.getKeyType(), sType.getValueType(), pMeta, keyIndent);
356            return m;
357         } else {
358            var m2 = newGenericMap();
359            Object value = readAnything(object(), r, m2, pMeta);
360            m2.put(s, value);
361            readBlockMappingRemainder(r, m2, string(), object(), pMeta, keyIndent);
362            if (m2.containsKey(getBeanTypePropertyName(eType)))
363               return cast(m2, pMeta, eType);
364            if (nn(sType.getProxyInvocationHandler()))
365               return checkRequired(newBeanMap(outer, sType.inner()).load(m2)).getBean();
366            throw new ParseException(this, "Class '%s' could not be instantiated.  Reason: '%s'", cn(sType), sType.getNotABeanReason());
367         }
368      }
369      if (sType.isObject())
370         return trim(s);
371      return convertToType(s, sType, eType, outer, pMeta);
372   }
373
374   private <T> Object handlePlainScalar(String s, ParserReader r, ClassMeta<?> sType, ClassMeta<?> eType, BuilderSwap<T,Object> builder, Object outer, BeanPropertyMeta pMeta, int keyIndent) throws IOException, ParseException, ExecutableException {
375      if (s.isEmpty())
376         return null;
377
378      if (looksLikeMappingKey(r)) {
379         r.read(); // consume ':'
380         int cp = r.peek();
381         if (cp == ' ')
382            r.read(); // consume space after ':'
383
384         String keyStr = isYamlNull(s) ? null : trim(s);
385         if (sType.isObject()) {
386            var m2 = newGenericMap();
387            Object value = readAnything(object(), r, m2, pMeta);
388            setName(object(), value, keyStr);
389            m2.put(keyStr, value);
390            readBlockMappingRemainder(r, m2, string(), object(), pMeta, keyIndent);
391            return cast(m2, pMeta, eType);
392         } else if (nn(builder)) {
393            var m = toBeanMap(builder.create(this, eType));
394            readBeanProperty(r, m, s);
395            readIntoBeanMapBlockRemainder(r, m, keyIndent);
396            return builder.build(this, checkRequired(m).getBean(), eType);
397         } else if (sType.canCreateNewBean(outer)) {
398            var m = newBeanMap(outer, sType.inner());
399            readBeanProperty(r, m, s);
400            readIntoBeanMapBlockRemainder(r, m, keyIndent);
401            return checkRequired(m).getBean();
402         } else if (sType.isMap()) {
403            Map m = (sType.canCreateNewInstance(outer) ? (Map)sType.newInstance(outer) : newGenericMap(sType));
404            Object value = readAnything(sType.getValueType(), r, m, pMeta);
405            Object key = convertAttrToType(m, keyStr, sType.getKeyType());
406            setName(sType.getValueType(), value, key);
407            m.put(key, value);
408            readBlockMappingRemainder(r, m, sType.getKeyType(), sType.getValueType(), pMeta, keyIndent);
409            return m;
410         } else {
411            var m2 = newGenericMap();
412            Object value = readAnything(object(), r, m2, pMeta);
413            m2.put(s, value);
414            readBlockMappingRemainder(r, m2, string(), object(), pMeta, keyIndent);
415            if (m2.containsKey(getBeanTypePropertyName(eType)))
416               return cast(m2, pMeta, eType);
417            if (nn(sType.getProxyInvocationHandler()))
418               return checkRequired(newBeanMap(outer, sType.inner()).load(m2)).getBean();
419            throw new ParseException(this, "Class '%s' could not be instantiated.  Reason: '%s'", cn(sType), sType.getNotABeanReason());
420         }
421      }
422
423      if (isYamlNull(s))
424         return null;
425
426      if ("true".equals(s) || "false".equals(s)) {
427         if (sType.isObject() || sType.isBoolean())
428            return Boolean.valueOf(s);
429         return convertToType(s, sType, eType, outer, pMeta);
430      }
431
432      return convertToType(s, sType, eType, outer, pMeta);
433   }
434
435   private Object convertToType(String s, ClassMeta<?> sType, ClassMeta<?> eType, Object outer, BeanPropertyMeta pMeta) throws ParseException {
436      if (sType.isObject()) {
437         return resolveScalarType(trim(s));
438      } else if (sType.isCharSequence()) {
439         return trim(s);
440      } else if (sType.isChar()) {
441         return parseCharacter(s);
442      } else if (sType.isNumber()) {
443         return StringUtils.parseNumber(s, (Class<? extends Number>)sType.inner());
444      } else if (sType.isBoolean()) {
445         return b(s);
446      } else if (sType.isDate()) {
447         return readDate(s, sType);
448      } else if (sType.isCalendar()) {
449         return readCalendar(s, sType);
450      } else if (sType.isTemporal()) {
451         return readTemporal(s, sType);
452      } else if (sType.isDuration()) {
453         return readDuration(s);
454      } else if (sType.isPeriod()) {
455         return readPeriod(s);
456      } else if (sType.canCreateNewInstanceFromString(outer)) {
457         return sType.newInstanceFromString(outer, s);
458      } else {
459         throw new ParseException(this, "Unrecognized syntax for class type '%s', value '%s'", sType, s);
460      }
461   }
462
463   private static boolean looksLikeMappingKey(ParserReader r) throws IOException {
464      return r.peek() == ':';
465   }
466
467   private static boolean isYamlNull(String s) {
468      return "null".equals(s) || "Null".equals(s) || "NULL".equals(s) || "~".equals(s);
469   }
470
471   private <K,V> void readBlockMappingRemainder(ParserReader r, Map<K,V> m, ClassMeta<K> keyType, ClassMeta<V> valueType, BeanPropertyMeta pMeta, int parentIndent) throws IOException, ParseException, ExecutableException {
472      readBlockMapping(r, m, keyType, valueType, pMeta, parentIndent);
473   }
474
475   private <T> void readIntoBeanMapBlockRemainder(ParserReader r, BeanMap<T> m, int parentIndent) throws IOException, ParseException, ExecutableException {
476      readIntoBeanMapBlock(r, m, parentIndent);
477   }
478
479   private <T> void readBeanProperty(ParserReader r, BeanMap<T> m, String currAttr) throws IOException, ParseException, ExecutableException {
480      var pb = swapParentBean(m.getBean(false));
481      try {
482      var pm = m.getPropertyMeta(currAttr);
483      setCurrentProperty(pm);
484      if (pm == null) {
485         onUnknownProperty(currAttr, m, readAnything(object(), r, m.getBean(false), null));
486      } else {
487         var cm = (ClassMeta<?>) pm.getBeanInfo();
488         Object value = readAnything(cm, r, m.getBean(false), pm);
489         setName(cm, value, currAttr);
490         try {
491            pm.set(m, currAttr, value);
492         } catch (BeanRuntimeException e) {
493            onBeanSetterException(pm, e);
494            throw e;
495         }
496      }
497      } finally {
498         swapParentBean(pb);
499      }
500      setCurrentProperty(null);
501   }
502
503   private static boolean isNullBlockValue(ParserReader r, int blockIndent) throws IOException {
504      int c = r.peek();
505      if (c == -1)
506         return true;
507      if (c != '\n' && c != '\r')
508         return false;
509      int charsRead = 0;
510      while (true) {
511         c = r.read();
512         if (c == -1) {
513            for (int j = 0; j < charsRead; j++) r.unread();
514            return true;
515         }
516         charsRead++;
517         if (c != '\n' && c != '\r' && c != ' ' && c != '\t')
518            break;
519      }
520      int nextColumn = r.getPosition().getColumn() - 1;
521      for (int j = 0; j < charsRead; j++) r.unread();
522      return nextColumn <= blockIndent;
523   }
524
525   private static int peekSecondChar(ParserReader r) throws IOException {
526      r.read();
527      int c2 = r.peek();
528      r.unread();
529      return c2;
530   }
531
532   // ==========================================
533   // Flow mapping: { key: value, ... }
534   // ==========================================
535   private <K,V> Map<K,V> readFlowMapping(ParserReader r, Map<K,V> m, ClassMeta<K> keyType, ClassMeta<V> valueType, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException {
536
537      if (keyType == null)
538         keyType = (ClassMeta<K>)string();
539
540      // S1: Looking for outer {
541      // S2: Looking for key start or }
542      // S3: Found key, looking for :
543      // S4: Found :, looking for value
544      // S5: Looking for , or }
545      // S6: Found , looking for key start
546
547      var state = S1;
548      String currKey = null;
549      int c = 0;
550      while (c != -1) {
551         c = r.read();
552         if (state == S1) {
553            if (c == '{')
554               state = S2;
555            else if (isWhitespace(c))
556               continue;
557            else
558               break;
559         } else if (state == S2) {
560            if (c == '}') {
561               return m;
562            } else if (isWhitespace(c)) {
563               continue;
564            } else {
565               r.unread();
566               currKey = readFlowMappingKey(r);
567               state = S3;
568            }
569         } else if (state == S3) {
570            if (c == ':')
571               state = S4;
572            else if (isWhitespace(c))
573               continue;
574         } else if (state == S4) {
575            if (isWhitespace(c))
576               continue;
577            r.unread();
578            K key = convertAttrToType(m, currKey, keyType);
579            V value = readAnything(valueType, r, m, pMeta);
580            setName(valueType, value, key);
581            m.put(key, value);
582            state = S5;
583         } else if (state == S5) {
584            if (c == ',') {
585               state = S6;
586            } else if (isWhitespace(c)) {
587               continue;
588            } else if (c == '}') {
589               return m;
590            } else {
591               break;
592            }
593         } else if (state == S6) {
594            if (c == '}') {
595               break;
596            } else if (isWhitespace(c)) {
597               continue;
598            } else {
599               r.unread();
600               currKey = readFlowMappingKey(r);
601               state = S3;
602            }
603         }
604      }
605      if (state == S1)
606         throw new ParseException(this, "Expected '{' at beginning of YAML flow mapping.");
607      if (state == S2)
608         throw new ParseException(this, "Could not find key in YAML flow mapping.");
609      if (state == S3)
610         throw new ParseException(this, "Could not find ':' following key in YAML flow mapping.");
611      if (state == S4)
612         throw new ParseException(this, "Expected value in YAML flow mapping.");
613      if (state == S5)
614         throw new ParseException(this, "Could not find '}' marking end of YAML flow mapping.");
615      if (state == S6)
616         throw new ParseException(this, "Unexpected '}' found in YAML flow mapping.");
617
618      return null; // Unreachable.
619   }
620
621   private String readFlowMappingKey(ParserReader r) throws IOException, ParseException {
622      int c = r.peek();
623      if (c == '\'' )
624         return readSingleQuotedString(r);
625      if (c == '"')
626         return readDoubleQuotedString(r);
627      var key = readPlainFlowKey(r);
628      if (isYamlNull(key))
629         return null;
630      return key;
631   }
632
633   private static String readPlainFlowKey(ParserReader r) throws IOException {
634      var sb = new StringBuilder();
635      int c;
636      while ((c = r.read()) != -1) {
637         if (c == ':' || c == ',' || c == '}' || c == '{' || c == '[' || c == ']') {
638            r.unread();
639            break;
640         }
641         sb.append((char)c);
642      }
643      return sb.toString().trim();
644   }
645
646   // ==========================================
647   // Flow sequence: [ value, ... ]
648   // ==========================================
649   private <E> Collection<E> readFlowSequence(ParserReader r, Collection<E> l, ClassMeta<?> type, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException {
650
651      // S1: Looking for outermost [
652      // S2: Looking for value or ]
653      // S3: Looking for , or ]
654      // S4: Looking for value
655
656      int argIndex = 0;
657
658      var state = S1;
659      int c = 0;
660      while (c != -1) {
661         c = r.read();
662         if (state == S1) {
663            if (c == '[')
664               state = S2;
665            else if (isWhitespace(c))
666               continue;
667            else
668               break;
669         } else if (state == S2) {
670            if (c == ']') {
671               return l;
672            } else if (isWhitespace(c)) {
673               continue;
674            } else if (c != -1) {
675               r.unread();
676               l.add((E)readAnything(type.isArgs() ? type.getArg(argIndex++) : type.getElementType(), r, l, pMeta));
677               state = S3;
678            }
679         } else if (state == S3) {
680            if (c == ',') {
681               state = S4;
682            } else if (isWhitespace(c)) {
683               continue;
684            } else if (c == ']') {
685               return l;
686            } else {
687               break;
688            }
689         } else if (state == S4) {
690            if (isWhitespace(c)) {
691               continue;
692            } else if (c == ']') {
693               break;
694            } else if (c != -1) {
695               r.unread();
696               l.add((E)readAnything(type.isArgs() ? type.getArg(argIndex++) : type.getElementType(), r, l, pMeta));
697               state = S3;
698            }
699         }
700      }
701      if (state == S1)
702         throw new ParseException(this, "Expected '[' at beginning of YAML flow sequence.");
703      if (state == S2)
704         throw new ParseException(this, "Expected value or ']' in YAML flow sequence.");
705      if (state == S3)
706         throw new ParseException(this, "Expected ',' or ']' in YAML flow sequence.");
707      if (state == S4)
708         throw new ParseException(this, "Unexpected trailing comma in YAML flow sequence.");
709
710      return null; // Unreachable.
711   }
712
713   // ==========================================
714   // Block mapping
715   // ==========================================
716   private <K,V> Map<K,V> readBlockMapping(ParserReader r, Map<K,V> m, ClassMeta<K> keyType, ClassMeta<V> valueType, BeanPropertyMeta pMeta, int parentIndent) throws IOException, ParseException, ExecutableException {
717
718      if (keyType == null)
719         keyType = (ClassMeta<K>)string();
720
721      int blockIndent = -1;
722
723      while (true) {
724         int consumed = skipBlanksAndCountIndent(r);
725         int lineIndent = r.getPosition().getColumn();
726         int c = r.peek();
727         if (c == -1)
728            break;
729
730         if (blockIndent == -1) {
731            if (lineIndent <= parentIndent && parentIndent > 0) {
732               unreadSpaces(r, consumed);
733               break;
734            }
735            blockIndent = lineIndent;
736         }
737
738         if (lineIndent < blockIndent) {
739            unreadSpaces(r, consumed);
740            break;
741         }
742
743         c = r.peek();
744         if (c == '#') {
745            skipToEndOfLine(r);
746            continue;
747         }
748
749         String keyStr = trim(readBlockMappingKey(r));
750
751         c = r.read(); // Should be ':'
752         if (c != ':')
753            throw new ParseException(this, "Expected ':' after key in YAML block mapping, found '%s'", (char)c);
754
755         c = r.peek();
756         if (c == ' ')
757            r.read();
758         K key = convertAttrToType(m, keyStr, keyType);
759         V value = (V)readAnything(valueType, r, m, pMeta);
760         setName(valueType, value, key);
761         m.put(key, value);
762      }
763
764      return m;
765   }
766
767   private static int skipBlanksAndCountIndent(ParserReader r) throws IOException {
768      int indent = 0;
769      int c;
770      while ((c = r.read()) != -1) {
771         if (c == ' ') {
772            indent++;
773         } else if (c == '\n' || c == '\r') {
774            indent = 0;
775         } else if (c == '\t') {
776            indent++;
777         } else {
778            r.unread();
779            break;
780         }
781      }
782      return indent;
783   }
784
785   private static void unreadSpaces(ParserReader r, int count) throws IOException {
786      for (int i = 0; i < count; i++)
787         r.unread();
788   }
789
790   private String readBlockMappingKey(ParserReader r) throws IOException, ParseException {
791      int c = r.peek();
792      if (c == '\'')
793         return readSingleQuotedString(r);
794      if (c == '"')
795         return readDoubleQuotedString(r);
796      var sb = new StringBuilder();
797      while ((c = r.read()) != -1) {
798         if (c == ':') {
799            int next = r.peek();
800            if (next == ' ' || next == '\n' || next == '\r' || next == -1) {
801               r.unread();
802               break;
803            }
804            sb.append((char)c);
805         } else if (c == '\n' || c == '\r') {
806            r.unread();
807            break;
808         } else {
809            sb.append((char)c);
810         }
811      }
812      String key = sb.toString().trim();
813      if (isYamlNull(key))
814         return null;
815      return key;
816   }
817
818   // ==========================================
819   // Block mapping for beans
820   // ==========================================
821   private <T> BeanMap<T> readIntoBeanMap(ParserReader r, BeanMap<T> m) throws IOException, ParseException, ExecutableException {
822      int c = r.peek();
823      if (c == '{') {
824         return readIntoBeanMapFlow(r, m);
825      }
826      return readIntoBeanMapBlock(r, m, 0);
827   }
828
829   private <T> BeanMap<T> readIntoBeanMapFlow(ParserReader r, BeanMap<T> m) throws IOException, ParseException, ExecutableException {
830
831      // S1: Looking for outer {
832      // S2: Looking for attrName start.
833      // S3: Found attrName end, looking for :.
834      // S4: Found :, looking for valStart.
835      // S5: Looking for , or }
836
837      var state = S1;
838      var currAttr = "";
839      int c = 0;
840      var pb = swapParentBean(m.getBean(false));
841      mark();
842      try {
843         while (c != -1) {
844            c = r.read();
845            if (state == S1) {
846               if (c == '{')
847                  state = S2;
848               else if (isWhitespace(c))
849                  continue;
850               else
851                  break;
852            } else if (state == S2) {
853               if (c == '}') {
854                  return m;
855               } else if (isWhitespace(c)) {
856                  continue;
857               } else {
858                  r.unread();
859                  mark();
860                  currAttr = readFlowMappingKey(r);
861                  state = S3;
862               }
863            } else if (state == S3) {
864               if (c == ':')
865                  state = S4;
866               else if (isWhitespace(c))
867                  continue;
868            } else if (state == S4) {
869               if (isWhitespace(c))
870                  continue;
871               if (! currAttr.equals(getBeanTypePropertyName((ClassMeta<?>) m.getBeanInfo()))) {
872                  var pm = m.getPropertyMeta(currAttr);
873                  setCurrentProperty(pm);
874                  if (pm == null) {
875                     onUnknownProperty(currAttr, m, readAnything(object(), r.unread(), m.getBean(false), null));
876                     unmark();
877                  } else {
878                     unmark();
879                     var cm = (ClassMeta<?>) pm.getBeanInfo();
880                     Object value = readAnything(cm, r.unread(), m.getBean(false), pm);
881                     setName(cm, value, currAttr);
882                     try {
883                        pm.set(m, currAttr, value);
884                     } catch (BeanRuntimeException e) {
885                        onBeanSetterException(pm, e);
886                        throw e;
887                     }
888                  }
889                  setCurrentProperty(null);
890               }
891               state = S5;
892            } else if (state == S5) {
893               if (c == ',')
894                  state = S2;
895               else if (isWhitespace(c))
896                  continue;
897               else if (c == '}') {
898                  return m;
899               }
900            }
901         }
902         if (state == S1)
903            throw new ParseException(this, "Expected '{' at beginning of YAML flow mapping.");
904         if (state == S2)
905            throw new ParseException(this, "Could not find attribute name in YAML flow mapping.");
906         if (state == S3)
907            throw new ParseException(this, "Could not find ':' following attribute name in YAML flow mapping.");
908         if (state == S4)
909            throw new ParseException(this, "Expected value in YAML flow mapping.");
910         if (state == S5)
911            throw new ParseException(this, "Could not find '}' marking end of YAML flow mapping.");
912      } finally {
913         unmark();
914         swapParentBean(pb);
915      }
916
917      return null; // Unreachable.
918   }
919
920   private <T> BeanMap<T> readIntoBeanMapBlock(ParserReader r, BeanMap<T> m, int parentIndent) throws IOException, ParseException, ExecutableException {
921
922      int blockIndent = -1;
923
924      var pb = swapParentBean(m.getBean(false));
925      mark();
926      try {
927         while (true) {
928            int consumed = skipBlanksAndCountIndent(r);
929            int lineIndent = r.getPosition().getColumn();
930            int c = r.peek();
931            if (c == -1)
932               break;
933
934            if (blockIndent == -1) {
935               if (lineIndent <= parentIndent && parentIndent > 0) {
936                  unreadSpaces(r, consumed);
937                  break;
938               }
939               blockIndent = lineIndent;
940            }
941
942            if (lineIndent < blockIndent) {
943               unreadSpaces(r, consumed);
944               break;
945            }
946
947            c = r.peek();
948            if (c == '#') {
949               skipToEndOfLine(r);
950               continue;
951            }
952
953            String currAttr = trim(readBlockMappingKey(r));
954
955            c = r.read(); // Should be ':'
956            if (c != ':')
957               throw new ParseException(this, "Expected ':' after attribute name in YAML block mapping, found '%s'", (char)c);
958
959            if (! currAttr.equals(getBeanTypePropertyName((ClassMeta<?>) m.getBeanInfo()))) {
960               var pm = m.getPropertyMeta(currAttr);
961               setCurrentProperty(pm);
962               if (pm == null) {
963                  c = r.peek();
964                  if (c == ' ')
965                     r.read();
966                  Object v = isNullBlockValue(r, blockIndent) ? null : readAnything(object(), r, m.getBean(false), null);
967                  onUnknownProperty(currAttr, m, v);
968                  unmark();
969               } else {
970                  unmark();
971                  var cm = (ClassMeta<?>) pm.getBeanInfo();
972                  c = r.peek();
973                  if (c == ' ')
974                     r.read();
975                  Object value = isNullBlockValue(r, blockIndent) ? null : readAnything(cm, r, m.getBean(false), pm);
976                  setName(cm, value, currAttr);
977                  try {
978                     pm.set(m, currAttr, value);
979                  } catch (BeanRuntimeException e) {
980                     onBeanSetterException(pm, e);
981                     throw e;
982                  }
983               }
984               setCurrentProperty(null);
985            }
986            mark();
987         }
988      } finally {
989         unmark();
990         swapParentBean(pb);
991      }
992
993      return m;
994   }
995
996   // ==========================================
997   // Block sequence: - value\n- value\n...
998   // ==========================================
999   private <E> Collection<E> readBlockSequence(ParserReader r, Collection<E> l, ClassMeta<?> type, BeanPropertyMeta pMeta, int parentIndent) throws IOException, ParseException, ExecutableException {
1000
1001      int blockIndent = -1;
1002      int argIndex = 0;
1003
1004      while (true) {
1005         int consumed = skipBlanksAndCountIndent(r);
1006         int lineIndent = r.getPosition().getColumn();
1007         int c = r.peek();
1008         if (c == -1)
1009            break;
1010
1011         if (blockIndent == -1) {
1012            if (lineIndent <= parentIndent && parentIndent > 0) {
1013               unreadSpaces(r, consumed);
1014               break;
1015            }
1016            blockIndent = lineIndent;
1017         }
1018
1019         if (lineIndent < blockIndent) {
1020            unreadSpaces(r, consumed);
1021            break;
1022         }
1023
1024         c = r.peek();
1025         if (c == '#') {
1026            skipToEndOfLine(r);
1027            continue;
1028         }
1029         if (c != '-') {
1030            unreadSpaces(r, lineIndent);
1031            break;
1032         }
1033
1034         r.read(); // consume '-'
1035         c = r.peek();
1036         if (c != ' ' && c != '\n' && c != '\r' && c != -1)
1037            throw new ParseException(this, "Expected space or line terminator after '-' in YAML block sequence, but found '%s'.", (char)c);
1038
1039         if (c == ' ')
1040            r.read(); // consume space after '-'
1041
1042         ClassMeta<?> elementType = type.isArgs() ? type.getArg(argIndex++) : type.getElementType();
1043         l.add((E)readAnything(elementType, r, l, pMeta));
1044      }
1045
1046      return l;
1047   }
1048
1049   // ==========================================
1050   // Single-quoted string: 'hello ''world'''
1051   // ==========================================
1052   private String readSingleQuotedString(ParserReader r) throws IOException, ParseException {
1053      int c = r.read(); // consume opening '
1054      if (c != '\'')
1055         throw new ParseException(this, "Expected single quote to start string.");
1056
1057      var sb = new StringBuilder();
1058      while ((c = r.read()) != -1) {
1059         if (c == '\'') {
1060            int next = r.peek();
1061            if (next == '\'') {
1062               r.read(); // consume escaped '
1063               sb.append('\'');
1064            } else {
1065               return sb.toString();
1066            }
1067         } else {
1068            sb.append((char)c);
1069         }
1070      }
1071      throw new ParseException(this, "Could not find end of single-quoted YAML string.");
1072   }
1073
1074   // ==========================================
1075   // Double-quoted string: "hello \"world\"\n"
1076   // ==========================================
1077   private String readDoubleQuotedString(ParserReader r) throws IOException, ParseException {
1078      int c = r.read(); // consume opening "
1079      if (c != '"')
1080         throw new ParseException(this, "Expected double quote to start string.");
1081
1082      var sb = new StringBuilder();
1083      while ((c = r.read()) != -1) {
1084         if (c == '\\') {
1085            c = r.read();
1086            // @formatter:off
1087            switch (c) {
1088               case '\\': sb.append('\\'); break;
1089               case '"': sb.append('"'); break;
1090               case 'n': sb.append('\n'); break;
1091               case 't': sb.append('\t'); break;
1092               case 'r': sb.append('\r'); break;
1093               case '0': sb.append('\0'); break;
1094               case 'a': sb.append('\u0007'); break;
1095               case 'b': sb.append('\b'); break;
1096               case 'f': sb.append('\f'); break;
1097               case 'e': sb.append('\u001b'); break;
1098               case '/': sb.append('/'); break;
1099               case 'x': {
1100                  String hex = r.read(2);
1101                  try {
1102                     sb.append((char)Integer.parseInt(hex, 16));
1103                  } catch (@SuppressWarnings("unused") NumberFormatException e) {
1104                     throw new ParseException(this, "Invalid \\x escape sequence in YAML string.");
1105                  }
1106                  break;
1107               }
1108               case 'u': {
1109                  String hex = r.read(4);
1110                  try {
1111                     sb.append((char)Integer.parseInt(hex, 16));
1112                  } catch (@SuppressWarnings("unused") NumberFormatException e) {
1113                     throw new ParseException(this, "Invalid \\u escape sequence in YAML string.");
1114                  }
1115                  break;
1116               }
1117               default:
1118                  throw new ParseException(this, "Invalid escape sequence '\\%s' in YAML string.", (char)c);
1119            }
1120            // @formatter:on
1121         } else if (c == '"') {
1122            return sb.toString();
1123         } else {
1124            sb.append((char)c);
1125         }
1126      }
1127      throw new ParseException(this, "Could not find end of double-quoted YAML string.");
1128   }
1129
1130   // ==========================================
1131   // Plain scalar (unquoted)
1132   // ==========================================
1133   private static String readPlainScalar(ParserReader r, int indent) throws IOException {
1134      var sb = new StringBuilder();
1135      int c;
1136      while ((c = r.read()) != -1) {
1137         if (c == '\n' || c == '\r') {
1138            r.unread();
1139            break;
1140         }
1141         if (c == ':') {
1142            int next = r.peek();
1143            if (next == ' ' || next == '\n' || next == '\r' || next == -1) {
1144               r.unread(); // unread ':'
1145               break;
1146            }
1147            sb.append((char)c);
1148         } else if (c == ' ') {
1149            int next = r.peek();
1150            if (next == '#') {
1151               // rest of line is comment
1152               r.unread();
1153               break;
1154            }
1155            sb.append((char)c);
1156         } else if ((c == ',' || c == '}' || c == ']') || (c == '#' && sb.isEmpty())) {
1157            r.unread();
1158            break;
1159         } else {
1160            sb.append((char)c);
1161         }
1162      }
1163      String result = sb.toString();
1164      // Trim trailing whitespace
1165      int end = result.length();
1166      while (end > 0 && result.charAt(end - 1) == ' ')
1167         end--;
1168      return result.substring(0, end);
1169   }
1170
1171   // ==========================================
1172   // Block scalar: | or >
1173   // ==========================================
1174   private String readBlockScalar(ParserReader r, char indicator) throws IOException, ParseException {
1175      r.read(); // consume '|' or '>'
1176
1177      // Parse indicator line for chomping
1178      char chomping = ' '; // default = clip
1179      int c;
1180      while ((c = r.read()) != -1) {
1181         if (c == '-') {
1182            chomping = '-'; // strip
1183         } else if (c == '+') {
1184            chomping = '+'; // keep
1185         } else if (c == '\n' || c == '\r') {
1186            if (c == '\r') {
1187               int next = r.peek();
1188               if (next == '\n')
1189                  r.read();
1190            }
1191            break;
1192         } else if (c >= '1' && c <= '9') {
1193            // explicit indent indicator, ignore for now (auto-detect below)
1194         } else if (c == ' ' || c == '\t') {
1195            continue;
1196         } else {
1197            throw new ParseException(this, "Unexpected character '%s' in YAML block scalar indicator.", (char)c);
1198         }
1199      }
1200
1201      // Read the block content
1202      int blockIndent = -1;
1203      var lines = new ArrayList<String>();
1204      int trailingNewlines = 0;
1205
1206      while (true) {
1207         // Count leading spaces
1208         int spaces = 0;
1209         while ((c = r.read()) != -1) {
1210            if (c == ' ')
1211               spaces++;
1212            else
1213               break;
1214         }
1215
1216         if (c == -1) {
1217            break;
1218         }
1219
1220         if (c == '\n' || c == '\r') {
1221            // Empty line.  Only tracked via trailingNewlines here -- the "" placeholder is added to
1222            // `lines` once, either by the flush below (once the next content line is reached, for an
1223            // interior blank run) or never at all (for a run of blank lines trailing the last content
1224            // line, which chomping='+' replays directly from trailingNewlines instead -- see below).
1225            trailingNewlines++;
1226            if (c == '\r' && r.peek() == '\n')
1227               r.read();
1228            continue;
1229         }
1230
1231         if (blockIndent == -1) {
1232            blockIndent = spaces;
1233         }
1234
1235         if (spaces < blockIndent) {
1236            // End of block, push back everything
1237            r.unread(); // push back non-space char
1238            for (int i = 0; i < spaces; i++)
1239               r.unread();
1240            break;
1241         }
1242
1243         // Read rest of line
1244         var lineSb = new StringBuilder();
1245         // Include extra spaces beyond blockIndent
1246         for (int i = blockIndent; i < spaces; i++)
1247            lineSb.append(' ');
1248         lineSb.append((char)c);
1249         while ((c = r.read()) != -1) {
1250            if (c == '\n' || c == '\r') {
1251               if (c == '\r' && r.peek() == '\n')
1252                  r.read();
1253               break;
1254            }
1255            lineSb.append((char)c);
1256         }
1257
1258         // Account for any blank lines that preceded this content line
1259         for (int i = 0; i < trailingNewlines; i++)
1260            lines.add("");
1261         trailingNewlines = 0;
1262         lines.add(lineSb.toString());
1263
1264         if (c == -1)
1265            break;
1266      }
1267
1268      // Build the result based on indicator and chomping
1269      var sb = new StringBuilder();
1270      for (int i = 0; i < lines.size(); i++) {
1271         String line = lines.get(i);
1272         if (indicator == '|') {
1273            // Literal: preserve newlines
1274            if (i > 0)
1275               sb.append('\n');
1276            sb.append(line);
1277         } else {
1278            // Folded: single newlines -> space, blank-line runs preserved as that many newlines.
1279            // Each blank line contributes its own '\n' via the "line.isEmpty()" arm below; a content
1280            // line immediately following a blank run needs no additional separator of its own (that
1281            // would double-count the boundary), so it's the "line.isEmpty()" check alone -- not an OR
1282            // against the *previous* line's emptiness -- that decides whether a newline is due here.
1283            if (i > 0) {
1284               if (line.isEmpty()) {
1285                  sb.append('\n');
1286               } else if (! lines.get(i - 1).isEmpty()) {
1287                  sb.append(' ');
1288               }
1289            }
1290            sb.append(line);
1291         }
1292      }
1293
1294      // Apply chomping
1295      if (chomping == '-') {
1296         // strip: no trailing newline
1297      } else if (chomping == '+') {
1298         // keep: preserve all trailing newlines
1299         sb.append('\n');
1300         for (int i = 0; i < trailingNewlines; i++)
1301            sb.append('\n');
1302      } else {
1303         // clip (default): single trailing newline
1304         sb.append('\n');
1305      }
1306
1307      return sb.toString();
1308   }
1309
1310   // ==========================================
1311   // Helper methods
1312   // ==========================================
1313
1314   private static void skipWhitespaceAndComments(ParserReader r) throws IOException {
1315      int c;
1316      while ((c = r.read()) != -1) {
1317         if (c == '#') {
1318            // Skip to end of line
1319            while ((c = r.read()) != -1) {
1320               if (c == '\n' || c == '\r')
1321                  break;
1322            }
1323         } else if (!isWhitespace(c)) {
1324            r.unread();
1325            return;
1326         }
1327      }
1328   }
1329
1330   private static void skipDocumentMarker(ParserReader r) throws IOException {
1331      int c = r.peek();
1332      if (c == '-') {
1333         r.read();
1334         int c2 = r.peek();
1335         if (c2 == '-') {
1336            r.read();
1337            int c3 = r.peek();
1338            if (c3 == '-') {
1339               r.read();
1340               // '---' found, skip rest of line
1341               skipToEndOfLine(r);
1342               return;
1343            }
1344            r.unread(); // unread second '-'
1345         }
1346         r.unread(); // unread first '-'
1347      } else if (c == '.') {
1348         r.read();
1349         int c2 = r.peek();
1350         if (c2 == '.') {
1351            r.read();
1352            int c3 = r.peek();
1353            if (c3 == '.') {
1354               r.read();
1355               // '...' found, skip rest of line
1356               skipToEndOfLine(r);
1357               return;
1358            }
1359            r.unread();
1360         }
1361         r.unread();
1362      }
1363   }
1364
1365   private static void skipToEndOfLine(ParserReader r) throws IOException {
1366      int c;
1367      while ((c = r.read()) != -1) {
1368         if (c == '\n' || c == '\r')
1369            return;
1370      }
1371   }
1372
1373   private static boolean isWhitespace(int c) {
1374      return c == ' ' || c == '\t' || c == '\n' || c == '\r';
1375   }
1376
1377   private static Object resolveScalarType(String s) {
1378      if (s == null || "null".equals(s) || "~".equals(s) || s.isEmpty())
1379         return null;
1380      if ("true".equals(s) || "True".equals(s) || "TRUE".equals(s))
1381         return Boolean.TRUE;
1382      if ("false".equals(s) || "False".equals(s) || "FALSE".equals(s))
1383         return Boolean.FALSE;
1384
1385      Object num = tryParseNumber(s);
1386      if (num != null)
1387         return num;
1388      return s;
1389   }
1390
1391   private static Object tryParseNumber(String s) {
1392      if (s.isEmpty())
1393         return null;
1394      char first = s.charAt(0);
1395      if (first != '-' && first != '+' && (first < '0' || first > '9') && first != '.')
1396         return null;
1397      try {
1398         if (s.contains(".") || s.contains("e") || s.contains("E")) {
1399            Double d = Double.parseDouble(s);
1400            return (!d.isInfinite() && !d.isNaN()) ? d : null;
1401         }
1402         return tryParseIntegerOrLong(s);
1403      } catch (@SuppressWarnings("unused") NumberFormatException e) {
1404         return null;
1405      }
1406   }
1407
1408   private static Object tryParseIntegerOrLong(String s) {
1409      try {
1410         return Integer.valueOf(s);
1411      } catch (@SuppressWarnings("unused") NumberFormatException e) {
1412         try {
1413            return Long.valueOf(s);
1414         } catch (@SuppressWarnings("unused") NumberFormatException e2) {
1415            return null;
1416         }
1417      }
1418   }
1419}