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.xml; 018 019import static javax.xml.stream.XMLStreamConstants.*; 020import static org.apache.juneau.commons.utils.CollectionUtils.*; 021import static org.apache.juneau.commons.utils.Shorts.*; 022import static org.apache.juneau.commons.utils.StringUtils.*; 023import static org.apache.juneau.marshall.xml.XmlFormat.*; 024 025import java.io.*; 026import java.lang.reflect.*; 027import java.util.*; 028 029import javax.xml.stream.*; 030import javax.xml.stream.util.*; 031 032import org.apache.juneau.commons.bean.*; 033import org.apache.juneau.commons.reflect.*; 034import org.apache.juneau.marshall.*; 035import org.apache.juneau.marshall.collections.*; 036import org.apache.juneau.marshall.parser.*; 037import org.apache.juneau.marshall.stream.*; 038import org.apache.juneau.marshall.swap.spi.*; 039 040/** 041 * Session object that lives for the duration of a single use of {@link XmlParser}. 042 * 043 * <h5 class='section'>Notes:</h5><ul> 044 * <li class='warn'>This class is not thread safe and is typically discarded after one use. 045 * </ul> 046 * 047 * <h5 class='section'>See Also:</h5><ul> 048 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/XmlSupport">XML</a> 049 050 * </ul> 051 */ 052@SuppressWarnings({ 053 "java:S110", // Deep inheritance inherent to the serializer/parser session hierarchy 054 "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants 055 "java:S3776", // readAnything(), readIntoBean(), getUnknown() and readIntoCollection() are branch-heavy XML event state machines; splitting them would obscure the grammar 056 "java:S6541", // readAnything() and readIntoBean() are long single-pass dispatches over XML element/attribute forms 057 "null", // Null handling verified by context or framework 058 "rawtypes", // Raw types necessary for generic type handling 059 "resource", // RecordReader returned by RecordAdapter is a Closeable owned by the caller; Eclipse JDT @Owning warning is by design. 060 "unchecked" // Type erasure requires unchecked casts 061}) 062public class XmlParserSession extends ReaderParserSession implements RecordReadable, ArrayRecordReadable { 063 064 /** 065 * Builder class. 066 */ 067 @SuppressWarnings({ 068 "java:S119" // 'SELF' (CRTP self-type) is intentional and clearer than a single-letter name. 069 }) 070 public abstract static class Builder<SELF extends Builder<SELF>> extends ReaderParserSession.Builder<SELF> { 071 072 private boolean preserveRootElement; 073 private boolean validating; 074 private final XmlParser ctx; 075 076 /** 077 * Constructor 078 * 079 * @param ctx The context creating this session. 080 * <br>Cannot be <jk>null</jk>. 081 */ 082 protected Builder(XmlParser ctx) { 083 super(ctx); 084 this.ctx = ctx; 085 preserveRootElement = ctx.isPreserveRootElement(); 086 validating = ctx.isValidating(); 087 } 088 089 @Override 090 public XmlParserSession build() { 091 return new XmlParserSession(this); 092 } 093 094 /** 095 * Preserve root element during XML parsing. 096 * 097 * @param value The new value for this setting. 098 * @return This object. 099 */ 100 public SELF preserveRootElement(boolean value) { 101 preserveRootElement = value; 102 return self(); 103 } 104 105 /** 106 * Validate XML during parsing. 107 * 108 * @param value The new value for this setting. 109 * @return This object. 110 */ 111 public SELF validating(boolean value) { 112 validating = value; 113 return self(); 114 } 115 116 @Override /* Overridden from Builder */ 117 public SELF property(String key, Object value) { 118 if (key == null) { super.property(key, value); return self(); } 119 switch (key) { 120 case "preserveRootElement", "XmlParserSession.preserveRootElement": 121 return preserveRootElement(cvt(value, Boolean.class)); 122 case "validating", "XmlParserSession.validating": 123 return validating(cvt(value, Boolean.class)); 124 default: 125 super.property(key, value); 126 return self(); 127 } 128 } 129 130 } 131 132 /** 133 * Concrete default builder leaf for the non-subclassed {@code create()} path (CRTP terminal). 134 */ 135 public static final class DefaultBuilder extends Builder<DefaultBuilder> { 136 137 DefaultBuilder(XmlParser ctx) { 138 super(ctx); 139 } 140 } 141 142 private static final int UNKNOWN = 0; 143 private static final int OBJECT = 1; 144 private static final int ARRAY = 2; 145 private static final int STRING = 3; 146 private static final int NUMBER = 4; 147 private static final int BOOLEAN = 5; 148 private static final int NULL = 6; 149 150 /** 151 * Creates a new builder for this object. 152 * 153 * @param ctx The context creating this session. 154 * <br>Cannot be <jk>null</jk>. 155 * @return A new builder. 156 */ 157 @SuppressWarnings({ 158 "java:S1452" // Builder<?> wildcard return intentional; callers use it to construct session instances polymorphically 159 }) 160 public static Builder<?> create(XmlParser ctx) { 161 return new DefaultBuilder(ctx); 162 } 163 164 private static int getJsonType(String s) { 165 if (s == null) 166 return UNKNOWN; 167 var c = s.charAt(0); 168 return switch (c) { 169 case 'o' -> (eq(s, "object") ? OBJECT : UNKNOWN); 170 case 'a' -> (eq(s, "array") ? ARRAY : UNKNOWN); 171 case 's' -> (eq(s, "string") ? STRING : UNKNOWN); 172 case 'b' -> (eq(s, "boolean") ? BOOLEAN : UNKNOWN); 173 case 'n' -> { 174 c = s.charAt(2); 175 yield switch (c) { 176 case 'm' -> (eq(s, "number") ? NUMBER : UNKNOWN); 177 case 'l' -> (eq(s, "null") ? NULL : UNKNOWN); 178 default -> NUMBER; 179 }; 180 } 181 default -> UNKNOWN; 182 }; 183 } 184 185 private final XmlParser ctx; 186 private final boolean preserveRootElement; 187 private final boolean validating; 188 189 private final StringBuilder rsb = new StringBuilder(); // Reusable string builder used in this class. 190 191 /** 192 * Constructor. 193 * 194 * @param builder The builder for this object. 195 */ 196 protected XmlParserSession(Builder<?> builder) { 197 super(builder); 198 ctx = builder.ctx; 199 preserveRootElement = builder.preserveRootElement; 200 validating = builder.validating; 201 } 202 203 /* 204 * Returns the name of the specified attribute on the current XML element. 205 * Any <js>'_x####_'</js> sequences in the string will be decoded. 206 */ 207 private String getAttributeName(XmlReader r, int i) { 208 return decodeString(r.getAttributeLocalName(i)); 209 } 210 211 /* 212 * Returns the value of the specified attribute on the current XML element. 213 * Any <js>'_x####_'</js> sequences in the string will be decoded. 214 */ 215 private String getAttributeValue(XmlReader r, int i) { 216 return decodeString(r.getAttributeValue(i)); 217 } 218 219 /** 220 * Takes the element being read from the XML stream reader and reconstructs it as XML. 221 * Used when reconstructing bean properties of type {@link XmlFormat#XMLTEXT}. 222 */ 223 private String getElementAsString(XmlReader r) { 224 int t = r.getEventType(); 225 if (t > 2) 226 throw rex("Invalid event type on stream reader for elementToString() method: '%s'", XmlUtils.toReadableEvent(r)); 227 rsb.setLength(0); 228 rsb.append("<").append(t == 1 ? "" : "/").append(r.getLocalName()); 229 if (t == 1) 230 for (var i = 0; i < r.getAttributeCount(); i++) 231 rsb.append(' ').append(r.getAttributeName(i)).append('=').append('\'').append(r.getAttributeValue(i)).append('\''); 232 rsb.append('>'); 233 return rsb.toString(); 234 } 235 236 /* 237 * Returns the name of the current XML element. 238 * Any <js>'_x####_'</js> sequences in the string will be decoded. 239 */ 240 private String getElementName(XmlReader r) { 241 return decodeString(r.getLocalName()); 242 } 243 244 /* 245 * Returns the _name attribute value. 246 * Any <js>'_x####_'</js> sequences in the string will be decoded. 247 */ 248 private String getNameProperty(XmlReader r) { 249 return decodeString(r.getAttributeValue(null, MarshallingSession.NAME_PROPERTY_NAME)); 250 } 251 252 /** 253 * Shortcut for calling <code>getText(r, <jk>true</jk>);</code>. 254 */ 255 private String getText(XmlReader r) { 256 return getText(r, true); 257 } 258 259 /* 260 * Returns the content of the current CHARACTERS node. 261 * Any <js>'_x####_'</js> sequences in the string will be decoded. 262 * Leading and trailing whitespace (unencoded) will be trimmed from the result. 263 */ 264 private String getText(XmlReader r, boolean trim) { 265 var s = r.getText(); 266 if (trim) 267 s = s.trim(); 268 if (s.isEmpty()) 269 return null; 270 return decodeString(s); 271 } 272 273 private Object getUnknown(XmlReader r) throws IOException, ParseException, ExecutableException, XMLStreamException { 274 if (r.getEventType() != START_ELEMENT) { 275 throw new ParseException(this, "Parser must be on START_ELEMENT to read next text."); 276 } 277 MarshalledMap m = null; 278 279 // If this element has attributes, then it's always a MarshalledMap. 280 if (r.getAttributeCount() > 0) { 281 m = newGenericMap(); 282 for (var i = 0; i < r.getAttributeCount(); i++) { 283 var key = getAttributeName(r, i); 284 var val = r.getAttributeValue(i); 285 if (! isSpecialAttr(key)) 286 m.put(key, val); 287 } 288 } 289 int eventType = r.next(); 290 var sb = getStringBuilder(); 291 while (eventType != END_ELEMENT) { 292 if (eventType == CHARACTERS || eventType == CDATA || eventType == XMLStreamConstants.SPACE || eventType == ENTITY_REFERENCE) { 293 sb.append(r.getText()); 294 } else if (eventType == PROCESSING_INSTRUCTION || eventType == COMMENT) { 295 // skipping 296 } else if (eventType == END_DOCUMENT) { 297 throw new ParseException(this, "Unexpected end of document when reading element text content"); 298 } else if (eventType == START_ELEMENT) { 299 // Oops...this has an element in it. 300 // Parse it as a map. 301 if (m == null) 302 m = newGenericMap(); 303 int depth = 0; 304 do { 305 int event = (eventType == -1 ? r.nextTag() : eventType); 306 String currAttr; 307 if (event == START_ELEMENT) { 308 depth++; 309 currAttr = getNameProperty(r); 310 if (currAttr == null) 311 currAttr = getElementName(r); 312 String key = convertAttrToType(null, currAttr, string()); 313 var value = readAnything(object(), currAttr, r, null, false, null); 314 if (m.containsKey(key)) { 315 var o = m.get(key); 316 if (o instanceof MarshalledList o2) 317 o2.add(value); 318 else 319 m.put(key, new JsonList(o, value).setBeanSession(this)); 320 } else { 321 m.put(key, value); 322 } 323 324 } else if (event == END_ELEMENT) { 325 break; 326 } 327 eventType = -1; 328 } while (depth > 0); 329 break; 330 } else { 331 throw new ParseException(this, "Unexpected event type '%s'", eventType); 332 } 333 eventType = r.next(); 334 } 335 var s = sb.toString().trim(); 336 returnStringBuilder(sb); 337 s = decodeString(s); 338 if (nn(m)) { 339 if (! s.isEmpty()) 340 m.put("contents", s); 341 return m; 342 } 343 return s; 344 } 345 346 private boolean isSpecialAttr(String key) { 347 return key.equals(getBeanTypePropertyName(null)) || key.equals(MarshallingSession.NAME_PROPERTY_NAME); 348 } 349 350 private <T> BeanMap<T> readIntoBean(XmlReader r, BeanMap<T> m, boolean isNil) throws IOException, ParseException, ExecutableException, XMLStreamException { 351 var bMeta = m.getMeta(); 352 var xmlMeta = getXmlBeanMeta(bMeta); 353 354 for (var i = 0; i < r.getAttributeCount(); i++) { 355 String key = getAttributeName(r, i); 356 if (! (eq(key, "nil") || isSpecialAttr(key))) { 357 var val = r.getAttributeValue(i); 358 var ns = r.getAttributeNamespace(i); 359 var bpm = xmlMeta.getPropertyMeta(key); 360 if (bpm == null) { 361 if (nn(xmlMeta.getAttrsProperty())) { 362 xmlMeta.getAttrsProperty().add(m, key, key, val); 363 } else if (ns == null) { 364 onUnknownProperty(key, m, val); 365 } 366 } else { 367 try { 368 bpm.set(m, key, val); 369 } catch (BeanRuntimeException e) { 370 onBeanSetterException(bpm, e); 371 throw e; 372 } 373 } 374 } 375 } 376 377 var cp = xmlMeta.getContentProperty(); 378 var cpf = xmlMeta.getContentFormat(); 379 var trim = cp == null || ! cpf.isOneOf(MIXED_PWS, TEXT_PWS, RAWTEXT); 380 ClassMeta<?> cpcm = (cp == null ? object() : (ClassMeta<?>) cp.getBeanInfo()); 381 StringBuilder sb = null; 382 var breg = cp == null ? null : cp.getBeanRegistry(); 383 List<Object> l = null; 384 385 int depth = 0; 386 do { 387 var event = r.next(); 388 String currAttr; 389 // We only care about text in MIXED mode. 390 // Ignore if in FREEFORM mode. 391 if (event == CHARACTERS) { 392 if (nn(cp) && cpf.isOneOf(MIXED, MIXED_PWS)) { 393 if (cpcm.isCollectionOrArray()) { 394 if (l == null) 395 l = ll(); 396 l.add(getText(r, false)); 397 } else { 398 cp.set(m, null, getText(r, trim)); 399 } 400 } else if (nn(cp) && cpf == RAWTEXT) { 401 // Verbatim: no trim, no _x####_ decoding (mirrors RAWTEXT serialize which bypasses encoding). 402 if (sb == null) 403 sb = getStringBuilder(); 404 sb.append(r.getText()); 405 } else if (cpf != FREEFORM) { 406 var s = getText(r, trim); 407 if (nn(s)) { 408 if (sb == null) 409 sb = getStringBuilder(); 410 sb.append(s); 411 } 412 } else { 413 // Do nothing...we're in FREEFORM mode. 414 } 415 } else if (event == START_ELEMENT) { 416 if (nn(cp) && cpf.isOneOf(TEXT, TEXT_PWS)) { 417 var s = readText(r); 418 if (nn(s)) { 419 if (sb == null) 420 sb = getStringBuilder(); 421 sb.append(s); 422 } 423 depth--; 424 } else if (cpf == XMLTEXT) { 425 if (sb == null) 426 sb = getStringBuilder(); 427 sb.append(getElementAsString(r)); 428 depth++; 429 } else if (nn(cp) && cpf.isOneOf(MIXED, MIXED_PWS)) { 430 if (isWhitespaceElement(r) && (breg == null || ! breg.hasName(r.getLocalName()))) { 431 if (cpcm.isCollectionOrArray()) { 432 if (l == null) 433 l = ll(); 434 l.add(readWhitespaceElement(r)); 435 } else { 436 cp.set(m, null, readWhitespaceElement(r)); 437 } 438 } else { 439 if (cpcm.isCollectionOrArray()) { 440 if (l == null) 441 l = ll(); 442 l.add(readAnything(cpcm.getElementType(), cp.getName(), r, m.getBean(false), false, cp)); 443 } else { 444 cp.set(m, null, readAnything(cpcm, cp.getName(), r, m.getBean(false), false, cp)); 445 } 446 } 447 } else if (nn(cp) && cpf == FREEFORM) { 448 cp.add(m, null, readAnything(cpcm.getElementType(), cp.getName(), r, m.getBean(false), false, cp)); 449 } else { 450 currAttr = getNameProperty(r); 451 if (currAttr == null) 452 currAttr = getElementName(r); 453 var pMeta = xmlMeta.getPropertyMeta(currAttr); 454 if (pMeta == null) { 455 var value = readAnything(object(), currAttr, r, m.getBean(false), false, null); 456 onUnknownProperty(currAttr, m, value); 457 } else { 458 setCurrentProperty(pMeta); 459 var xf = getXmlBeanPropertyMeta(pMeta).getXmlFormat(); 460 if (xf == COLLAPSED) { 461 var et = ((ClassMeta<?>) pMeta.getBeanInfo()).getElementType(); 462 var value = readAnything(et, currAttr, r, m.getBean(false), false, pMeta); 463 setName(et, value, currAttr); 464 pMeta.add(m, currAttr, value); 465 } else if (xf == ATTR) { 466 pMeta.set(m, currAttr, getAttributeValue(r, 0)); 467 r.nextTag(); 468 } else { 469 var cm = (ClassMeta<?>) pMeta.getBeanInfo(); 470 var value = readAnything(cm, currAttr, r, m.getBean(false), false, pMeta); 471 setName(cm, value, currAttr); 472 pMeta.set(m, currAttr, value); 473 } 474 setCurrentProperty(null); 475 } 476 } 477 } else if (event == END_ELEMENT) { 478 if (depth > 0) { 479 if (cpf == XMLTEXT) { 480 if (sb == null) 481 sb = getStringBuilder(); 482 sb.append(getElementAsString(r)); 483 } else 484 throw new ParseException("End element found where one was not expected. %s", XmlUtils.toReadableEvent(r)); 485 } 486 depth--; 487 } else if (event == COMMENT) { 488 // Ignore comments. 489 } else { 490 throw new ParseException("Unexpected event type: %s", XmlUtils.toReadableEvent(r)); 491 } 492 } while (depth >= 0); 493 494 if (nn(cp) && ! isNil) { 495 if (nn(sb)) 496 cp.set(m, null, sb.toString()); 497 else if (nn(l)) 498 cp.set(m, null, XmlUtils.collapseTextNodes(l)); 499 else if (cpcm.isCollectionOrArray()) { 500 var o = cp.get(m, null); 501 if (o == null) 502 cp.set(m, cp.getName(), list()); 503 } 504 } 505 506 returnStringBuilder(sb); 507 return m; 508 } 509 510 private <E> Collection<E> readIntoCollection(XmlReader r, Collection<E> l, ClassMeta<?> type, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException, XMLStreamException { 511 int depth = 0; 512 int argIndex = 0; 513 do { 514 var event = r.nextTag(); 515 if (event == START_ELEMENT) { 516 depth++; 517 ClassMeta<?> elementType; 518 if (type == null) { 519 elementType = object(); 520 } else if (type.isArgs()) { 521 elementType = type.getArg(argIndex++); 522 } else { 523 elementType = type.getElementType(); 524 } 525 E value = (E)readAnything(elementType, null, r, l, false, pMeta); 526 l.add(value); 527 } else if (event == END_ELEMENT) { 528 return l; 529 } 530 } while (depth > 0); 531 return l; 532 } 533 534 private <K,V> Map<K,V> readIntoMap(XmlReader r, Map<K,V> m, ClassMeta<K> keyType, ClassMeta<V> valueType, BeanPropertyMeta pMeta) 535 throws IOException, ParseException, ExecutableException, XMLStreamException { 536 int depth = 0; 537 for (var i = 0; i < r.getAttributeCount(); i++) { 538 var a = r.getAttributeLocalName(i); 539 if (! isSpecialAttr(a)) { 540 K key = trim(convertAttrToType(m, a, keyType)); 541 V value = trim(convertAttrToType(m, r.getAttributeValue(i), valueType)); 542 setName(valueType, value, key); 543 m.put(key, value); 544 } 545 } 546 do { 547 var event = r.nextTag(); 548 String currAttr; 549 if (event == START_ELEMENT) { 550 depth++; 551 currAttr = getNameProperty(r); 552 if (currAttr == null) 553 currAttr = getElementName(r); 554 K key = convertAttrToType(m, currAttr, keyType); 555 V value = readAnything(valueType, currAttr, r, m, false, pMeta); 556 setName(valueType, value, currAttr); 557 if (valueType.isObject() && m.containsKey(key)) { 558 var o = m.get(key); 559 if (o instanceof List o2) 560 o2.add(value); 561 else 562 m.put(key, (V)new JsonList(o, value).setBeanSession(this)); 563 } else { 564 m.put(key, value); 565 } 566 } else if (event == END_ELEMENT) { 567 return m; 568 } 569 } while (depth > 0); 570 return m; 571 } 572 573 /** 574 * Decodes and trims the specified string. 575 * 576 * <p> 577 * Any <js>'_x####_'</js> sequences in the string will be decoded. 578 * 579 * @param s The string to be decoded. 580 * <br>Can be <jk>null</jk> (returns <jk>null</jk>). 581 * @return The decoded string, or <jk>null</jk> if the input was <jk>null</jk>. 582 */ 583 protected final String decodeString(String s) { 584 if (s == null) 585 return null; 586 rsb.setLength(0); 587 s = XmlUtils.decode(s, rsb); 588 if (isTrimStrings()) 589 s = s.trim(); 590 return s; 591 } 592 593 @Override /* Overridden from ParserSession */ 594 protected <T> T doRead(ParserPipe pipe, ClassMeta<T> type) throws IOException, ParseException, ExecutableException { 595 try { 596 return readAnything(type, null, getXmlReader(pipe), getOuter(), true, null); 597 } catch (XMLStreamException e) { 598 throw new ParseException(e); 599 } 600 } 601 602 @Override /* Overridden from ReaderParserSession */ 603 protected <E> Collection<E> doReadIntoCollection(ParserPipe pipe, Collection<E> c, Type elementType) throws Exception { 604 var cm = getClassMeta(c.getClass(), elementType); 605 return readIntoCollection(getXmlReader(pipe), c, cm.getElementType(), null); 606 } 607 608 @Override /* Overridden from ReaderParserSession */ 609 protected <K,V> Map<K,V> doReadIntoMap(ParserPipe pipe, Map<K,V> m, Type keyType, Type valueType) throws Exception { 610 return readIntoMap(getXmlReader(pipe), m, (ClassMeta<K>)getClassMeta(keyType), (ClassMeta<V>)getClassMeta(valueType), null); 611 } 612 613 /** 614 * Opens a whole-value pull-parser cursor over an XML document, bound to this live session. 615 * 616 * <p> 617 * Because XML's wire format (attributes, namespaces, mixed content) is non-trivial to expose 618 * as a fine-grained token cursor, this implementation supports only the cursor-level POJO 619 * bridge — {@link RecordReader#read(Class) read(...)} delegates to the polymorphic 620 * {@link ParserSession#read(Object, Class)} entry point. 621 * 622 * @param input The input. 623 * @return A new {@link RecordReader} cursor. 624 * @throws IOException If a problem occurred opening the underlying input. 625 */ 626 @Override /* RecordReadable */ 627 public RecordReader readRecords(Object input) throws IOException { 628 return RecordAdapter.reader(this, input); 629 } 630 631 /** 632 * Buffered array-element {@link RecordReader} for XML, bound to this live session. Calls 633 * {@code parse(input, List.class, Object.class)} once and iterates the result. 634 * 635 * <p> 636 * For caller-specified-root semantics use {@link #readArrayRecords(Object, String)}. 637 * 638 * @param input The input. 639 * @return A buffered {@link RecordReader}. 640 * @throws IOException If a problem occurred reading the input. 641 */ 642 @Override /* ArrayRecordReadable */ 643 public RecordReader readArrayRecords(Object input) throws IOException { 644 return RecordAdapter.arrayReader(this, input); 645 } 646 647 /** 648 * Buffered array-element {@link RecordReader} that honors the caller-specified root element 649 * name. 650 * 651 * <p> 652 * The document is parsed once into a generic structure and the records to yield are selected by 653 * the supplied {@code rootElementName}: 654 * <ul> 655 * <li>If the document root is a <b>wrapper element</b> (parses to a {@link Map}) that contains a 656 * child named {@code rootElementName}, that child's value supplies the records (a repeated 657 * child element parses to a {@link List}; a single occurrence yields one record). Sibling 658 * elements with other names are ignored. 659 * <li>If the document root is an <b>array wrapper</b> (parses to a {@link List}, e.g. Juneau's 660 * {@code <array>} envelope), its already-unwrapped elements are the records and the hint is a 661 * no-op. 662 * <li>A scalar root yields a single record. 663 * </ul> 664 * 665 * <p> 666 * A <jk>null</jk> {@code rootElementName} is equivalent to {@link #readArrayRecords(Object)}. 667 * The cursor is buffered ({@link RecordReader#isStreaming()} == <jk>false</jk>); see 668 * {@code 175ab} Item 3 for why true element-at-a-time XML streaming is left for a 669 * demand-driven StAX cursor. 670 * 671 * @param input The input. 672 * @param rootElementName The name of the wrapping/child element whose occurrences are the 673 * records. <jk>null</jk> defers to {@link #readArrayRecords(Object)}. 674 * @return A buffered {@link RecordReader}. 675 * @throws IOException If a problem occurred reading the input. 676 */ 677 @Override /* ArrayRecordReadable */ 678 public RecordReader readArrayRecords(Object input, String rootElementName) throws IOException { 679 if (rootElementName == null) 680 return readArrayRecords(input); 681 Object parsed; 682 try { 683 parsed = read(input, Object.class); 684 } catch (ParseException e) { 685 throw new IOException(e); 686 } 687 var records = selectRootElements(parsed, rootElementName); 688 var iter = records.iterator(); 689 return new RecordReader() { 690 @Override public boolean canRead() { return iter.hasNext(); } 691 @Override public <T> T read(Class<T> type) { return convertToType(nextElement(), type); } 692 @Override public <T> T read(ClassMeta<T> type) { return convertToType(nextElement(), type); } 693 @Override public <T> T read(Type type, Type... args) { return XmlParserSession.this.<T>convertToType(nextElement(), type, args); } 694 @Override public boolean isStreaming() { return false; } 695 @Override public void close() throws IOException { 696 if (input instanceof Closeable input2) 697 input2.close(); 698 } 699 private Object nextElement() { 700 if (! iter.hasNext()) 701 throw new IllegalStateException("Array stream is exhausted."); 702 return iter.next(); 703 } 704 }; 705 } 706 707 /** Selects the records to yield from a generically-parsed XML document, honoring the root-element hint. */ 708 private static List<Object> selectRootElements(Object parsed, String rootElementName) { 709 if (parsed == null) 710 return Collections.emptyList(); 711 if (parsed instanceof Map<?,?> parsed2) { 712 var v = parsed2.get(rootElementName); 713 if (v == null) 714 return Collections.emptyList(); 715 if (v instanceof List<?> v2) 716 return new ArrayList<>(v2); 717 return new ArrayList<>(Collections.singletonList(v)); 718 } 719 if (parsed instanceof List<?> parsed2) 720 return new ArrayList<>(parsed2); 721 return new ArrayList<>(Collections.singletonList(parsed)); 722 } 723 724 /** 725 * The XML record cursor is buffered/{@link RecordAdapter}-backed, not O(1) streaming. 726 * 727 * @return Always <jk>false</jk>. 728 */ 729 @Override /* RecordReadable */ 730 public boolean isRecordStreaming() { return false; } 731 732 /** 733 * The XML array-record cursor is buffered/{@link RecordAdapter}-backed, not O(1) streaming. 734 * 735 * @return Always <jk>false</jk>. 736 */ 737 @Override /* ArrayRecordReadable */ 738 public boolean isArrayRecordStreaming() { return false; } 739 740 /** 741 * Returns the text content of the current XML element. 742 * 743 * <p> 744 * Any <js>'_x####_'</js> sequences in the string will be decoded. 745 * 746 * <p> 747 * Leading and trailing whitespace (unencoded) will be trimmed from the result. 748 * 749 * @param r The reader to read the element text from. 750 * <br>Must not be <jk>null</jk>. 751 * @return The decoded text. <jk>null</jk> if the text consists of the sequence <js>'_x0000_'</js>. 752 * @throws XMLStreamException Thrown by underlying reader. 753 * @throws IOException Thrown by underlying stream. 754 * @throws ParseException Malformed input encountered. 755 */ 756 protected String getElementText(XmlReader r) throws XMLStreamException, IOException, ParseException { 757 return decodeString(r.getElementText().trim()); 758 } 759 760 /** 761 * XML event allocator. 762 * 763 * @see XmlParser.Builder#eventAllocator(Class) 764 * @return 765 * The {@link XMLEventAllocator} associated with this parser, or <jk>null</jk> if there isn't one. 766 */ 767 protected final XMLEventAllocator getEventAllocator() { return ctx.getEventAllocator(); } 768 769 /** 770 * XML reporter. 771 * 772 * @see XmlParser.Builder#reporter(Class) 773 * @return 774 * The {@link XMLReporter} associated with this parser, or <jk>null</jk> if there isn't one. 775 */ 776 protected final XMLReporter getReporter() { return ctx.getReporter(); } 777 778 /** 779 * XML resolver. 780 * 781 * @see XmlParser.Builder#resolver(Class) 782 * @return 783 * The {@link XMLResolver} associated with this parser, or <jk>null</jk> if there isn't one. 784 */ 785 protected final XMLResolver getResolver() { return ctx.getResolver(); } 786 787 /** 788 * Returns the language-specific metadata on the specified bean. 789 * 790 * @param bm The bean to return the metadata on. 791 * @return The metadata. 792 */ 793 protected XmlBeanMeta getXmlBeanMeta(BeanMeta<?> bm) { 794 return ctx.getXmlBeanMeta(bm); 795 } 796 797 /** 798 * Returns the language-specific metadata on the specified bean property. 799 * 800 * @param bpm The bean property to return the metadata on. 801 * @return The metadata. 802 */ 803 protected XmlBeanPropertyMeta getXmlBeanPropertyMeta(BeanPropertyMeta bpm) { 804 return ctx.getXmlBeanPropertyMeta(bpm); 805 } 806 807 /** 808 * Returns the language-specific metadata on the specified class. 809 * 810 * @param cm The class to return the metadata on. 811 * @return The metadata. 812 */ 813 protected XmlClassMeta getXmlClassMeta(ClassMeta<?> cm) { 814 return ctx.getXmlClassMeta(cm); 815 } 816 817 /** 818 * Wrap the specified reader in a STAX reader based on settings in this context. 819 * 820 * @param pipe The parser input. 821 * <br>Must not be <jk>null</jk>. 822 * @return The new STAX reader. 823 * @throws IOException Thrown by underlying stream. 824 * @throws XMLStreamException Unexpected XML processing error. 825 */ 826 protected final XmlReader getXmlReader(ParserPipe pipe) throws IOException, XMLStreamException { 827 return new XmlReader(pipe, () -> ctx.getInputFactory(validating)); 828 } 829 830 /** 831 * Preserve root element during generalized parsing. 832 * 833 * @see XmlParser.Builder#preserveRootElement() 834 * @return 835 * <jk>true</jk> if when parsing into a generic {@link JsonMap}, the map will contain a single entry whose key 836 * is the root element name. 837 */ 838 protected final boolean isPreserveRootElement() { return preserveRootElement; } 839 840 /** 841 * Enable validation. 842 * 843 * @see XmlParser.Builder#validating() 844 * @return 845 * <jk>true</jk> if XML document will be validated. 846 */ 847 protected final boolean isValidating() { return validating; } 848 849 /** 850 * Returns <jk>true</jk> if the current element is a whitespace element. 851 * 852 * <p> 853 * For the XML parser, this always returns <jk>false</jk>. 854 * However, the HTML parser defines various whitespace elements such as <js>"br"</js> and <js>"sp"</js>. 855 * 856 * @param r The XML stream reader to read the current event from. 857 * @return <jk>true</jk> if the current element is a whitespace element. 858 */ 859 protected boolean isWhitespaceElement(XmlReader r) { 860 return false; 861 } 862 863 /** 864 * Workhorse method. 865 * 866 * @param <T> The expected type of object. 867 * @param eType The expected type of object. 868 * <br>Can be <jk>null</jk>, in which case the generic {@link Object} type is used and the actual type is auto-detected from the XML. 869 * @param currAttr The current bean property name. 870 * <br>Can be <jk>null</jk> if not being parsed as a named bean property (e.g. the root value). 871 * @param r The reader. 872 * <br>Must not be <jk>null</jk>. 873 * @param outer The outer object. 874 * <br>Can be <jk>null</jk> if there is no enclosing (outer) object; the parsed object is then not linked to a parent. 875 * @param isRoot If <jk>true</jk>, then we're serializing a root element in the document. 876 * @param pMeta The bean property metadata. 877 * <br>Can be <jk>null</jk> if no bean property metadata applies (e.g. the root value or an unknown property). 878 * @return The parsed object, or <jk>null</jk> if the element represents a <jk>null</jk> value. 879 * @throws IOException Thrown by underlying stream. 880 * @throws ParseException Malformed input encountered. 881 * @throws ExecutableException Exception occurred on invoked constructor/method/field. 882 * @throws XMLStreamException Malformed XML encountered. 883 */ 884 protected <T> T readAnything(ClassMeta<T> eType, String currAttr, XmlReader r, Object outer, boolean isRoot, BeanPropertyMeta pMeta) 885 throws IOException, ParseException, ExecutableException, XMLStreamException { 886 887 if (eType == null) 888 eType = (ClassMeta<T>)object(); 889 var swap = (ObjectSwap<T,Object>)eType.getSwap(this); 890 var builder = (BuilderSwap<T,Object>)eType.getBuilderSwap(this); 891 ClassMeta<?> sType = null; 892 if (nn(builder)) 893 sType = builder.getBuilderClassMeta(this); 894 else if (nn(swap)) 895 sType = swap.getSwapClassMeta(this); 896 else 897 sType = eType; 898 899 if (sType.isOptional()) 900 return (T)o(readAnything(eType.getElementType(), currAttr, r, outer, isRoot, pMeta)); 901 902 setCurrentClass(sType); 903 904 var wrapperAttr = (isRoot && isPreserveRootElement()) ? r.getName().getLocalPart() : null; 905 var typeAttr = r.getAttributeValue(null, getBeanTypePropertyName(eType)); 906 var isNil = eq(r.getAttributeValue(null, "nil"), "true"); 907 var jsonType = getJsonType(typeAttr); 908 var elementName = getElementName(r); 909 if (jsonType == 0) { 910 if (elementName == null || elementName.equals(currAttr)) 911 jsonType = UNKNOWN; 912 else { 913 typeAttr = elementName; 914 jsonType = getJsonType(elementName); 915 } 916 } 917 918 ClassMeta tcm = getClassMeta(typeAttr, pMeta, eType); 919 if (tcm == null && nn(elementName) && ! elementName.equals(currAttr)) 920 tcm = getClassMeta(elementName, pMeta, eType); 921 if (nn(tcm)) 922 sType = eType = tcm; 923 924 Object o = null; 925 926 if (jsonType == NULL) { 927 r.nextTag(); // Discard end tag 928 return null; 929 } 930 931 if (sType.isObject()) { 932 if (jsonType == OBJECT) { 933 var m = newGenericMap(); 934 readIntoMap(r, m, string(), object(), pMeta); 935 if (nn(wrapperAttr)) 936 m = newGenericMap().append(wrapperAttr, m); 937 o = cast(m, pMeta, eType); 938 } else if (jsonType == ARRAY) 939 o = readIntoCollection(r, newGenericList(), null, pMeta); 940 else if (jsonType == STRING) { 941 o = getElementText(r); 942 if (sType.isChar()) 943 o = parseCharacter(o); 944 } else if (jsonType == NUMBER) 945 o = parseNumber(getElementText(r), null); 946 else if (jsonType == BOOLEAN) 947 o = Boolean.parseBoolean(getElementText(r)); 948 else if (jsonType == UNKNOWN) 949 o = getUnknown(r); 950 } else if (nn(builder) || sType.canCreateNewBean(outer)) { 951 if (getXmlClassMeta(sType).getFormat() == COLLAPSED) { 952 var fieldName = r.getLocalName(); 953 var m = nn(builder) ? toBeanMap(builder.create(this, eType)) : newBeanMap(outer, sType.inner()); 954 var bpm = getXmlBeanMeta(m.getMeta()).getPropertyMeta(fieldName); 955 var cm = (ClassMeta<?>) m.getMeta().getBeanInfo(); 956 var pb = swapParentBean(m.getBean(false)); 957 try { 958 Object value = readAnything(cm, currAttr, r, m.getBean(false), false, null); 959 setName(cm, value, currAttr); 960 bpm.set(m, currAttr, value); 961 } finally { 962 swapParentBean(pb); 963 } 964 o = nn(builder) ? builder.build(this, checkRequired(m).getBean(), eType) : checkRequired(m).getBean(); 965 } else { 966 var m = nn(builder) ? toBeanMap(builder.create(this, eType)) : newBeanMap(outer, sType.inner()); 967 var pb = swapParentBean(m.getBean(false)); 968 try { 969 m = readIntoBean(r, m, isNil); 970 } finally { 971 swapParentBean(pb); 972 } 973 o = nn(builder) ? builder.build(this, checkRequired(m).getBean(), eType) : checkRequired(m).getBean(); 974 } 975 } else if (sType.isMap()) { 976 var m = (sType.canCreateNewInstance(outer) ? (Map)sType.newInstance(outer) : newGenericMap(sType)); 977 o = readIntoMap(r, m, sType.getKeyType(), sType.getValueType(), pMeta); 978 if (nn(wrapperAttr)) { 979 var wrapper = (sType.canCreateNewInstance(outer) ? (Map)sType.newInstance(outer) : newGenericMap()); 980 wrapper.put(wrapperAttr, m); 981 o = wrapper; 982 } 983 } else if (sType.isCollection()) { 984 var l = (sType.canCreateNewInstance(outer) ? (Collection)sType.newInstance(outer) : newGenericList()); 985 o = readIntoCollection(r, l, sType, pMeta); 986 } else if (sType.isArray() || sType.isArgs()) { 987 var l = (ArrayList)readIntoCollection(r, list(), sType, pMeta); 988 o = toArray(sType, l); 989 } else if (sType.isCharSequence()) { 990 o = getElementText(r); 991 } else if (sType.isChar()) { 992 o = parseCharacter(getElementText(r)); 993 } else if (sType.isNumber()) { 994 o = parseNumber(getElementText(r), (Class<? extends Number>)sType.inner()); 995 } else if (sType.isBoolean()) { 996 o = Boolean.parseBoolean(getElementText(r)); 997 } else if (sType.isDate()) { 998 o = readDate(getElementText(r), sType); 999 } else if (sType.isCalendar()) { 1000 o = readCalendar(getElementText(r), sType); 1001 } else if (sType.isTemporal()) { 1002 o = readTemporal(getElementText(r), sType); 1003 } else if (sType.isDuration()) { 1004 o = readDuration(getElementText(r)); 1005 } else if (sType.isPeriod()) { 1006 o = readPeriod(getElementText(r)); 1007 } else if (sType.canCreateNewInstanceFromString(outer)) { 1008 o = sType.newInstanceFromString(outer, getElementText(r)); 1009 } else if (nn(sType.getProxyInvocationHandler())) { 1010 var m = newGenericMap(); 1011 readIntoMap(r, m, string(), object(), pMeta); 1012 if (nn(wrapperAttr)) 1013 m = newGenericMap().append(wrapperAttr, m); 1014 o = checkRequired(newBeanMap(outer, sType.inner()).load(m)).getBean(); 1015 } else { 1016 throw new ParseException(this, "Class '%s' could not be instantiated. Reason: '%s', property: '%s'", cn(sType), sType.getNotABeanReason(), 1017 pMeta == null ? null : pMeta.getName()); 1018 } 1019 1020 if (nn(swap) && nn(o)) 1021 o = unswap(swap, o, eType); 1022 1023 if (nn(parentBean())) 1024 setParent(eType, o, parentBean()); 1025 1026 return (T)o; 1027 } 1028 1029 /** 1030 * Parses the current element as text. 1031 * 1032 * @param r The input reader. 1033 * <br>Must not be <jk>null</jk>. 1034 * @return The parsed text. 1035 * @throws XMLStreamException Thrown by underlying reader. 1036 * @throws IOException Thrown by underlying stream. 1037 * @throws ParseException Malformed input encountered. 1038 */ 1039 protected String readText(XmlReader r) throws IOException, XMLStreamException, ParseException { 1040 // Note that this is different than {@link #getText(XmlReader)} since it assumes that we're pointing to a 1041 // whitespace element. 1042 1043 var sb2 = getStringBuilder(); 1044 1045 int depth = 0; 1046 while (true) { 1047 var et = r.getEventType(); 1048 if (et == START_ELEMENT) { 1049 sb2.append(getElementAsString(r)); 1050 depth++; 1051 } else if (et == CHARACTERS) { 1052 sb2.append(getText(r)); 1053 } else if (et == END_ELEMENT) { 1054 sb2.append(getElementAsString(r)); 1055 depth--; 1056 if (depth <= 0) 1057 break; 1058 } 1059 r.next(); 1060 } 1061 var s = sb2.toString(); 1062 returnStringBuilder(sb2); 1063 return s; 1064 } 1065 1066 /** 1067 * Parses the current whitespace element. 1068 * 1069 * <p> 1070 * For the XML parser, this always returns <jk>null</jk> since there is no concept of a whitespace element. 1071 * However, the HTML parser defines various whitespace elements such as <js>"br"</js> and <js>"sp"</js>. 1072 * 1073 * @param r The XML stream reader to read the current event from. 1074 * @return The whitespace character or characters. 1075 * @throws XMLStreamException Thrown by underlying reader. 1076 * @throws IOException Thrown by underlying stream. 1077 * @throws ParseException Malformed input encountered. 1078 */ 1079 protected String readWhitespaceElement(XmlReader r) throws IOException, XMLStreamException, ParseException { 1080 return null; 1081 } 1082}