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 — 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}