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.html; 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.html.HtmlTag.*; 024 025import java.io.*; 026import java.lang.reflect.*; 027import java.util.*; 028 029import javax.xml.stream.*; 030 031import org.apache.juneau.commons.bean.*; 032import org.apache.juneau.commons.lang.*; 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.swap.spi.*; 038import org.apache.juneau.marshall.xml.*; 039 040/** 041 * ContextSession object that lives for the duration of a single use of {@link HtmlParser}. 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/HtmlSupport">HTML</a> 049 050 * </ul> 051 */ 052@SuppressWarnings({ 053 "java:S110", // Inheritance depth acceptable for this class hierarchy 054 "java:S115", // Constants use UPPER_snakeCase naming convention 055 "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants 056 "java:S2177", // Private readIntoBean/readIntoCollection/readIntoMap intentionally reuse the names of XmlParserSession methods with HTML-specific logic 057 "java:S3776", // readAnything, readTableIntoCollection and skipTag branch over HTML tag and element types 058 "rawtypes", // Raw types necessary for generic type handling 059 "unchecked" // Type erasure requires unchecked casts 060}) 061public class HtmlParserSession extends XmlParserSession { 062 063 /** 064 * Builder class. 065 */ 066 public static class Builder extends XmlParserSession.Builder<Builder> { 067 068 private final HtmlParser ctx; 069 070 /** 071 * Constructor 072 * 073 * @param ctx The context creating this session. 074 * <br>Cannot be <jk>null</jk>. 075 */ 076 protected Builder(HtmlParser ctx) { 077 super(reqnn("ctx", ctx)); 078 this.ctx = ctx; 079 } 080 081 @Override 082 public HtmlParserSession build() { 083 return new HtmlParserSession(this); 084 } 085 086 } 087 088 private static final Set<String> whitespaceElements = set("br", "bs", "sp", "ff"); 089 090 /** 091 * Creates a new builder for this object. 092 * 093 * @param ctx The context creating this session. 094 * <br>Cannot be <jk>null</jk>. 095 * @return A new builder. 096 */ 097 public static Builder create(HtmlParser ctx) { 098 return new Builder(reqnn("ctx", ctx)); 099 } 100 101 private static String getAttribute(XmlReader r, String name, String def) { 102 for (var i = 0; i < r.getAttributeCount(); i++) 103 if (r.getAttributeLocalName(i).equals(name)) 104 return r.getAttributeValue(i); 105 return def; 106 } 107 108 private static Map<String,String> getAttributes(XmlReader r) { 109 var m = new TreeMap<String,String>(); 110 for (var i = 0; i < r.getAttributeCount(); i++) 111 m.put(r.getAttributeLocalName(i), r.getAttributeValue(i)); 112 return m; 113 } 114 115 private static int skipWs(XmlReader r) throws XMLStreamException { 116 int event = r.getEventType(); 117 while (event != START_ELEMENT && event != END_ELEMENT && event != END_DOCUMENT && r.isWhiteSpace()) 118 event = r.next(); 119 return event; 120 } 121 122 private final HtmlParser ctx; 123 124 /** 125 * Constructor. 126 * 127 * @param builder The builder for this object. 128 */ 129 protected HtmlParserSession(Builder builder) { 130 super(builder); 131 ctx = builder.ctx; 132 } 133 134 /* 135 * Reads the next tag. Advances past anything that's not a start or end tag. Throws an exception if 136 * it's not one of the expected tags. 137 * Precondition: Must be pointing before the event we want to parse. 138 * Postcondition: Pointing at the tag just parsed. 139 */ 140 private HtmlTag nextTag(XmlReader r, HtmlTag...expected) throws ParseException, XMLStreamException { 141 int et = r.next(); 142 143 while (et != START_ELEMENT && et != END_ELEMENT && et != END_DOCUMENT) 144 et = r.next(); 145 146 if (et == END_DOCUMENT) 147 throw new ParseException(this, "Unexpected end of document."); 148 149 var tag = HtmlTag.forEvent(this, r); 150 if (expected.length == 0) 151 return tag; 152 for (var t : expected) 153 if (t == tag) 154 return tag; 155 156 throw new ParseException(this, "Unexpected tag: '%s'. Expected one of the following: %s", tag, expected); 157 } 158 159 /* 160 * Reads an anchor tag and converts it into a bean. 161 */ 162 private <T> T readAnchor(XmlReader r, ClassMeta<T> beanType) throws IOException, ParseException, XMLStreamException { 163 String href = r.getAttributeValue(null, "href"); 164 String name = getElementText(r); 165 if (nn(beanType) && getAnnotationProvider().has(HtmlLink.class, beanType)) { 166 var uriProperty = Holder.<String>empty(); 167 var nameProperty = Holder.<String>empty(); 168 beanType.forEachAnnotation(HtmlLink.class, x -> ine(x.uriProperty()), x -> uriProperty.set(x.uriProperty())); 169 beanType.forEachAnnotation(HtmlLink.class, x -> ine(x.nameProperty()), x -> nameProperty.set(x.nameProperty())); 170 BeanMap<T> m = newBeanMap(beanType.inner()); 171 m.put(uriProperty.orElse(""), href); 172 m.put(nameProperty.orElse(""), name); 173 return checkRequired(m).getBean(); 174 } 175 return convertToType(href, beanType); 176 } 177 178 /* 179 * Reads anything starting at the current event. 180 * <p> 181 * Precondition: Must be pointing at outer START_ELEMENT. 182 * Postcondition: Pointing at outer END_ELEMENT. 183 */ 184 private <T> T readAnything(ClassMeta<T> eType, XmlReader r, Object outer, boolean isRoot, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException, XMLStreamException { 185 186 if (eType == null) 187 eType = (ClassMeta<T>)object(); 188 var swap = (ObjectSwap<T,Object>)eType.getSwap(this); 189 var builder = (BuilderSwap<T,Object>)eType.getBuilderSwap(this); 190 ClassMeta<?> sType = null; 191 if (nn(builder)) 192 sType = builder.getBuilderClassMeta(this); 193 else if (nn(swap)) 194 sType = swap.getSwapClassMeta(this); 195 else 196 sType = eType; 197 198 if (sType.isOptional()) 199 return (T)o(readAnything(eType.getElementType(), r, outer, isRoot, pMeta)); 200 201 setCurrentClass(sType); 202 203 int event = r.getEventType(); 204 if (event != START_ELEMENT) 205 throw new ParseException(this, "readAnything must be called on outer start element."); 206 207 if (! isRoot) 208 event = r.next(); 209 var isEmpty = (event == END_ELEMENT); 210 211 // Skip until we find a start element, end document, or non-empty text. 212 if (! isEmpty) 213 event = skipWs(r); 214 215 if (event == END_DOCUMENT) 216 throw new ParseException(this, "Unexpected end of stream in readAnything for type '%s'", eType); 217 218 // Handle @Html(asXml=true) beans. 219 var hcm = getHtmlClassMeta(sType); 220 if (hcm.getFormat() == HtmlFormat.XML) 221 return super.readAnything(eType, null, r, outer, false, pMeta); 222 223 Object o = null; 224 225 var isValid = true; 226 var tag = (event == CHARACTERS ? null : HtmlTag.forString(r.getName().getLocalPart(), false)); 227 228 // If it's not a known tag, then parse it as XML. 229 // Allows us to parse stuff like "<div/>" into HTML5 beans. 230 if (tag == null && event != CHARACTERS) 231 return super.readAnything(eType, null, r, outer, false, pMeta); 232 233 if (tag == HTML) 234 tag = skipToData(r); 235 236 if (isEmpty) { 237 o = ""; 238 } else if (tag == null || tag.isOneOf(BR, BS, FF, SP)) { 239 String text = readText(r); 240 if (sType.isObject() || sType.isCharSequence()) 241 o = text; 242 else if (sType.isChar()) 243 o = parseCharacter(text); 244 else if (sType.isNumber()) 245 o = parseNumber(text, (Class<? extends Number>)eType.inner()); 246 else if (sType.isBoolean()) 247 o = Boolean.parseBoolean(text); 248 else if (sType.isDate()) 249 o = readDate(text, sType); 250 else if (sType.isCalendar()) 251 o = readCalendar(text, sType); 252 else if (sType.isTemporal()) 253 o = readTemporal(text, sType); 254 else if (sType.isDuration()) 255 o = readDuration(text); 256 else if (sType.isPeriod()) 257 o = readPeriod(text); 258 else if (sType.canCreateNewInstanceFromString(outer)) 259 o = sType.newInstanceFromString(outer, text); 260 else 261 isValid = false; 262 263 } else if (tag == STRING || (tag == A && nn(pMeta) && nn(getHtmlBeanPropertyMeta(pMeta).getLink()))) { 264 String text = getElementText(r); 265 if (sType.isObject() || sType.isCharSequence()) 266 o = text; 267 else if (sType.isChar()) 268 o = parseCharacter(text); 269 else if (sType.isDate()) 270 o = readDate(text, sType); 271 else if (sType.isCalendar()) 272 o = readCalendar(text, sType); 273 else if (sType.isTemporal()) 274 o = readTemporal(text, sType); 275 else if (sType.isDuration()) 276 o = readDuration(text); 277 else if (sType.isPeriod()) 278 o = readPeriod(text); 279 else if (sType.canCreateNewInstanceFromString(outer)) 280 o = sType.newInstanceFromString(outer, text); 281 else 282 isValid = false; 283 skipTag(r, tag == STRING ? X_STRING : X_A); 284 285 } else if (tag == NUMBER) { 286 String text = getElementText(r); 287 if (sType.isObject()) 288 o = parseNumber(text, Number.class); 289 else if (sType.isNumber()) 290 o = parseNumber(text, (Class<? extends Number>)sType.inner()); 291 else 292 isValid = false; 293 skipTag(r, X_NUMBER); 294 295 } else if (tag == BOOLEAN) { 296 String text = getElementText(r); 297 if (sType.isObject() || sType.isBoolean()) 298 o = Boolean.parseBoolean(text); 299 else 300 isValid = false; 301 skipTag(r, X_BOOLEAN); 302 303 } else if (tag == P) { 304 String text = getElementText(r); 305 if (neq(text, "No Results")) 306 isValid = false; 307 skipTag(r, X_P); 308 309 } else if (tag == NULL) { 310 skipTag(r, NULL); 311 skipTag(r, X_NULL); 312 313 } else if (tag == A) { 314 o = readAnchor(r, swap == null ? eType : null); 315 skipTag(r, X_A); 316 317 } else if (tag == TABLE) { 318 319 String typeName = getAttribute(r, getBeanTypePropertyName(eType), "object"); 320 ClassMeta cm = getClassMeta(typeName, pMeta, eType); 321 322 if (nn(cm)) { 323 sType = eType = cm; 324 typeName = sType.isCollectionOrArray() ? "array" : "object"; 325 } else if (neq(typeName, "array")) { 326 // Type name could be a subtype name. 327 typeName = sType.isCollectionOrArray() ? "array" : "object"; 328 } 329 330 if (typeName.equals("object")) { 331 if (sType.isObject()) { 332 o = readIntoMap(r, newGenericMap(sType), sType.getKeyType(), sType.getValueType(), pMeta); 333 } else if (nn(builder)) { 334 BeanMap m = toBeanMap(builder.create(this, eType)); 335 o = builder.build(this, checkRequired(readIntoBean(r, m)).getBean(), eType); 336 } else if (sType.canCreateNewBean(outer)) { 337 BeanMap m = newBeanMap(outer, sType.inner()); 338 o = checkRequired(readIntoBean(r, m)).getBean(); 339 } else if (sType.isMap()) { 340 o = readIntoMap(r, (Map)(sType.canCreateNewInstance(outer) ? sType.newInstance(outer) : newGenericMap(sType)), sType.getKeyType(), sType.getValueType(), pMeta); 341 } else if (nn(sType.getProxyInvocationHandler())) { 342 BeanMap m = newBeanMap(outer, sType.inner()); 343 o = checkRequired(readIntoBean(r, m)).getBean(); 344 } else { 345 isValid = false; 346 } 347 skipTag(r, X_TABLE); 348 349 } else if (typeName.equals("array")) { 350 if (sType.isObject()) 351 o = readTableIntoCollection(r, (Collection)newGenericList(), sType, pMeta); 352 else if (sType.isCollection()) 353 o = readTableIntoCollection(r, (Collection)(sType.canCreateNewInstance(outer) ? sType.newInstance(outer) : newGenericList()), sType, pMeta); 354 else if (sType.isArray() || sType.isArgs()) { 355 var l = (ArrayList)readTableIntoCollection(r, list(), sType, pMeta); 356 o = toArray(sType, l); 357 } else 358 isValid = false; 359 skipTag(r, X_TABLE); 360 361 } else { 362 isValid = false; 363 } 364 365 } else if (tag == UL) { 366 String typeName = getAttribute(r, getBeanTypePropertyName(eType), "array"); 367 ClassMeta cm = getClassMeta(typeName, pMeta, eType); 368 if (nn(cm)) 369 sType = eType = cm; 370 371 if (sType.isObject()) 372 o = readIntoCollection(r, newGenericList(), sType, pMeta); 373 else if (sType.isCollection() || sType.isObject()) 374 o = readIntoCollection(r, (Collection)(sType.canCreateNewInstance(outer) ? sType.newInstance(outer) : newGenericList()), sType, pMeta); 375 else if (sType.isArray() || sType.isArgs()) 376 o = toArray(sType, readIntoCollection(r, list(), sType, pMeta)); 377 else 378 isValid = false; 379 skipTag(r, X_UL); 380 381 } 382 383 if (! isValid) 384 throw new ParseException(this, "Unexpected tag '%s' for type '%s'", tag, eType); 385 386 if (nn(swap) && nn(o)) 387 o = unswap(swap, o, eType); 388 389 if (nn(parentBean())) 390 setParent(eType, o, parentBean()); 391 392 skipWs(r); 393 return (T)o; 394 } 395 396 /* 397 * Reads contents of <table> element. 398 * Precondition: Must be pointing at event following <table> event. 399 * Postcondition: Pointing at next START_ELEMENT or END_DOCUMENT event. 400 */ 401 private <T> BeanMap<T> readIntoBean(XmlReader r, BeanMap<T> m) throws IOException, ParseException, ExecutableException, XMLStreamException { 402 var pb = swapParentBean(m.getBean(false)); 403 try { 404 while (true) { 405 HtmlTag tag = nextTag(r, TR, X_TABLE); 406 if (tag == X_TABLE) 407 break; 408 tag = nextTag(r, TD, TH); 409 // Skip over the column headers. 410 if (tag == TH) { 411 skipTag(r); 412 r.nextTag(); 413 skipTag(r); 414 } else { 415 String key = getElementText(r); 416 nextTag(r, TD); 417 var pMeta = m.getPropertyMeta(key); 418 if (pMeta == null) { 419 onUnknownProperty(key, m, readAnything(object(), r, null, false, null)); 420 } else { 421 var cm = (ClassMeta<?>) pMeta.getBeanInfo(); 422 Object value = readAnything(cm, r, m.getBean(false), false, pMeta); 423 setName(cm, value, key); 424 try { 425 pMeta.set(m, key, value); 426 } catch (BeanRuntimeException e) { 427 onBeanSetterException(pMeta, e); 428 throw e; 429 } 430 } 431 } 432 HtmlTag t = nextTag(r, X_TD, X_TR); 433 if (t == X_TD) 434 nextTag(r, X_TR); 435 } 436 return m; 437 } finally { 438 swapParentBean(pb); 439 } 440 } 441 442 /* 443 * Reads contents of <ul> element. 444 * Precondition: Must be pointing at event following <ul> event. 445 * Postcondition: Pointing at next START_ELEMENT or END_DOCUMENT event. 446 */ 447 private <E> Collection<E> readIntoCollection(XmlReader r, Collection<E> l, ClassMeta<?> type, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException, XMLStreamException { 448 int argIndex = 0; 449 while (true) { 450 HtmlTag tag = nextTag(r, LI, X_UL, X_LI); 451 if (tag == X_LI) 452 tag = nextTag(r, LI, X_UL, X_LI); 453 if (tag == X_UL) 454 break; 455 var elementType = type.isArgs() ? type.getArg(argIndex++) : type.getElementType(); 456 l.add((E)readAnything(elementType, r, l, false, pMeta)); 457 } 458 return l; 459 } 460 461 /* 462 * Reads contents of <table> element. 463 * Precondition: Must be pointing at <table> event. 464 * Postcondition: Pointing at next START_ELEMENT or END_DOCUMENT event. 465 */ 466 private <K,V> Map<K,V> readIntoMap(XmlReader r, Map<K,V> m, ClassMeta<K> keyType, ClassMeta<V> valueType, BeanPropertyMeta pMeta) 467 throws IOException, ParseException, ExecutableException, XMLStreamException { 468 while (true) { 469 HtmlTag tag = nextTag(r, TR, X_TABLE); 470 if (tag == X_TABLE) 471 break; 472 tag = nextTag(r, TD, TH); 473 // Skip over the column headers. 474 if (tag == TH) { 475 skipTag(r); 476 r.nextTag(); 477 skipTag(r); 478 } else { 479 K key = readAnything(keyType, r, m, false, pMeta); 480 nextTag(r, TD); 481 V value = readAnything(valueType, r, m, false, pMeta); 482 setName(valueType, value, key); 483 m.put(key, value); 484 } 485 tag = nextTag(r, X_TD, X_TR); 486 if (tag == X_TD) 487 nextTag(r, X_TR); 488 } 489 490 return m; 491 } 492 493 /* 494 * Reads contents of <ul> element. 495 * Precondition: Must be pointing at event following <ul> event. 496 * Postcondition: Pointing at next START_ELEMENT or END_DOCUMENT event. 497 */ 498 private <E> Collection<E> readTableIntoCollection(XmlReader r, Collection<E> l, ClassMeta<E> type, BeanPropertyMeta pMeta) 499 throws IOException, ParseException, ExecutableException, XMLStreamException { 500 501 nextTag(r, TR); 502 List<String> keys = list(); 503 HtmlTag tag; 504 while (true) { 505 tag = nextTag(r, TH, X_TR); 506 if (tag == X_TR) 507 break; 508 keys.add(getElementText(r)); 509 } 510 511 int argIndex = 0; 512 513 while (true) { 514 r.nextTag(); 515 tag = HtmlTag.forEvent(this, r); 516 if (tag == X_TABLE) 517 break; 518 519 ClassMeta elementType = null; 520 String beanType = getAttribute(r, getBeanTypePropertyName(type), null); 521 if (nn(beanType)) 522 elementType = getClassMeta(beanType, pMeta, null); 523 if (elementType == null) 524 elementType = type.isArgs() ? type.getArg(argIndex++) : type.getElementType(); 525 if (elementType == null) 526 elementType = object(); 527 528 BuilderSwap<E,Object> builder = elementType.getBuilderSwap(this); 529 530 if (nn(builder) || elementType.canCreateNewBean(l)) { 531 // @formatter:off 532 BeanMap m = 533 nn(builder) 534 ? toBeanMap(builder.create(this, elementType)) 535 : newBeanMap(l, elementType.inner()) 536 ; 537 // @formatter:on 538 var pb = swapParentBean(m.getBean(false)); 539 try { 540 for (var key : keys) { 541 tag = nextTag(r, X_TD, TD, NULL); 542 if (tag == X_TD) 543 tag = nextTag(r, TD, NULL); 544 if (tag == NULL) { 545 m = null; 546 nextTag(r, X_NULL); 547 break; 548 } 549 BeanMapEntry e = m.getProperty(key); 550 if (e == null) { 551 readAnything(object(), r, l, false, null); 552 } else { 553 BeanPropertyMeta bpm = e.getMeta(); 554 var cm = (ClassMeta<?>) bpm.getBeanInfo(); 555 Object value = readAnything(cm, r, m.getBean(false), false, bpm); 556 setName(cm, value, key); 557 bpm.set(m, key, value); 558 } 559 } 560 } finally { 561 swapParentBean(pb); 562 } 563 E element; 564 if (m == null) { 565 element = null; 566 } else if (nn(builder)) { 567 element = builder.build(this, checkRequired(m).getBean(), elementType); 568 } else { 569 element = (E)checkRequired(m).getBean(); 570 } 571 if (nn(parentBean()) && nn(element)) 572 setParent(elementType, element, parentBean()); 573 l.add(element); 574 } else { 575 String c = getAttributes(r).get(getBeanTypePropertyName(type.getElementType())); 576 var m = (Map)(elementType.isMap() && elementType.canCreateNewInstance(l) ? elementType.newInstance(l) : newGenericMap(elementType)); 577 for (var key : keys) { 578 tag = nextTag(r, TD, NULL); 579 if (tag == NULL) { 580 m = null; 581 nextTag(r, X_NULL); 582 break; 583 } 584 if (nn(m)) { 585 var kt = elementType.getKeyType(); 586 var vt = elementType.getValueType(); 587 Object value = readAnything(vt, r, l, false, pMeta); 588 setName(vt, value, key); 589 m.put(convertToType(key, kt), value); 590 } 591 } 592 if (nn(m) && nn(c)) { 593 var m2 = (m instanceof MarshalledMap m3 ? m3 : new JsonMap(m).session(this)); 594 m2.put(getBeanTypePropertyName(type.getElementType()), c); 595 l.add((E)cast(m2, pMeta, elementType)); 596 } else { 597 if (m instanceof MarshalledMap m2) 598 l.add((E)convertToType(m2, elementType)); 599 else 600 l.add((E)m); 601 } 602 } 603 nextTag(r, X_TR); 604 } 605 return l; 606 } 607 608 /** 609 * Skips over the current element and advances to the next element. 610 * <p> 611 * Precondition: Pointing to opening tag. 612 * Postcondition: Pointing to next opening tag. 613 * 614 * @param r The stream being read from. 615 * @throws XMLStreamException 616 */ 617 private void skipTag(XmlReader r) throws ParseException, XMLStreamException { 618 int et = r.getEventType(); 619 620 if (et != START_ELEMENT) 621 throw new ParseException(this, "skipToNextTag() call on invalid event '%s'. Must only be called on START_ELEMENT events.", XmlUtils.toReadableEvent(r)); 622 623 String n = r.getLocalName(); 624 625 int depth = 0; 626 while (true) { 627 et = r.next(); 628 if (et == START_ELEMENT) { 629 String n2 = r.getLocalName(); 630 if (n.equals(n2)) 631 depth++; 632 } else if (et == END_ELEMENT) { 633 String n2 = r.getLocalName(); 634 if (n.equals(n2)) 635 depth--; 636 if (depth < 0) 637 return; 638 } 639 } 640 } 641 642 private void skipTag(XmlReader r, HtmlTag...expected) throws ParseException, XMLStreamException { 643 var tag = HtmlTag.forEvent(this, r); 644 if (tag.isOneOf(expected)) 645 r.next(); 646 else 647 throw new ParseException(this, "Unexpected tag: '%s'. Expected one of the following: %s", tag, expected); 648 } 649 650 /* 651 * For parsing output from HtmlDocSerializer, this skips over the head, title, and links. 652 */ 653 private HtmlTag skipToData(XmlReader r) throws ParseException, XMLStreamException { 654 while (true) { 655 var event = r.next(); 656 if (event == START_ELEMENT && eq(r.getLocalName(), "div") && eq(r.getAttributeValue(null, "id"), "data")) { 657 r.nextTag(); 658 event = r.getEventType(); 659 var isEmpty = (event == END_ELEMENT); 660 // Skip until we find a start element, end document, or non-empty text. 661 if (! isEmpty) 662 event = skipWs(r); 663 if (event == END_DOCUMENT) 664 throw new ParseException(this, "Unexpected end of stream looking for data."); 665 return (event == CHARACTERS ? null : HtmlTag.forString(r.getName().getLocalPart(), false)); 666 } 667 } 668 } 669 670 @Override /* Overridden from ParserSession */ 671 protected <T> T doRead(ParserPipe pipe, ClassMeta<T> type) throws IOException, ParseException, ExecutableException { 672 try { 673 return readAnything(type, getXmlReader(pipe), getOuter(), true, null); 674 } catch (XMLStreamException e) { 675 throw new ParseException(e); 676 } 677 } 678 679 @Override /* Overridden from ReaderParserSession */ 680 protected <E> Collection<E> doReadIntoCollection(ParserPipe pipe, Collection<E> c, Type elementType) throws Exception { 681 return readIntoCollection(getXmlReader(pipe), c, getClassMeta(elementType), null); 682 } 683 684 @Override /* Overridden from ReaderParserSession */ 685 protected <K,V> Map<K,V> doReadIntoMap(ParserPipe pipe, Map<K,V> m, Type keyType, Type valueType) throws Exception { 686 return readIntoMap(getXmlReader(pipe), m, (ClassMeta<K>)getClassMeta(keyType), (ClassMeta<V>)getClassMeta(valueType), null); 687 } 688 689 /** 690 * Identical to {@link #readText(XmlReader)} except assumes the current event is the opening tag. 691 * 692 * <p> 693 * Precondition: Pointing to opening tag. 694 * Postcondition: Pointing to closing tag. 695 * 696 * @param r The stream being read from. 697 * @return The parsed string. 698 * @throws XMLStreamException Thrown by underlying XML stream. 699 * @throws ParseException Malformed input encountered. 700 */ 701 @Override /* Overridden from XmlParserSession */ 702 protected String getElementText(XmlReader r) throws IOException, XMLStreamException, ParseException { 703 r.next(); 704 return readText(r); 705 } 706 707 /** 708 * Returns the language-specific metadata on the specified bean property. 709 * 710 * @param bpm The bean property to return the metadata on. 711 * @return The metadata. 712 */ 713 protected HtmlBeanPropertyMeta getHtmlBeanPropertyMeta(BeanPropertyMeta bpm) { 714 return ctx.getHtmlBeanPropertyMeta(bpm); 715 } 716 717 /** 718 * Returns the language-specific metadata on the specified class. 719 * 720 * @param cm The class to return the metadata on. 721 * @return The metadata. 722 */ 723 protected HtmlClassMeta getHtmlClassMeta(ClassMeta<?> cm) { 724 return ctx.getHtmlClassMeta(cm); 725 } 726 727 @Override /* Overridden from XmlParserSession */ 728 protected boolean isWhitespaceElement(XmlReader r) { 729 String s = r.getLocalName(); 730 return whitespaceElements.contains(s); 731 } 732 733 /** 734 * Parses CHARACTERS data. 735 * 736 * <p> 737 * Precondition: Pointing to event immediately following opening tag. 738 * Postcondition: Pointing to closing tag. 739 * 740 * @param r The stream being read from. 741 * @return The parsed string. 742 * @throws XMLStreamException Thrown by underlying XML stream. 743 */ 744 @Override /* Overridden from XmlParserSession */ 745 protected String readText(XmlReader r) throws IOException, ParseException, XMLStreamException { 746 747 StringBuilder sb = getStringBuilder(); 748 749 int et = r.getEventType(); 750 if (et == END_ELEMENT) 751 return ""; 752 753 int depth = 0; 754 755 String characters = null; 756 757 while (true) { 758 if (et == START_ELEMENT) { 759 if (nn(characters)) { 760 if (sb.isEmpty()) 761 characters = trimStart(characters); 762 sb.append(characters); 763 characters = null; 764 } 765 var tag = HtmlTag.forEvent(this, r); 766 if (tag == BR) { 767 sb.append('\n'); 768 r.nextTag(); 769 } else if (tag == BS) { 770 sb.append('\b'); 771 r.nextTag(); 772 } else if (tag == SP) { 773 et = r.next(); 774 if (et == CHARACTERS) { 775 String s = r.getText(); 776 if (ine(s)) { 777 var c = r.getText().charAt(0); 778 if (c == '\u2003') 779 c = '\t'; 780 sb.append(c); 781 } 782 r.nextTag(); 783 } 784 } else if (tag == FF) { 785 sb.append('\f'); 786 r.nextTag(); 787 } else if (tag.isOneOf(STRING, NUMBER, BOOLEAN)) { 788 et = r.next(); 789 if (et == CHARACTERS) { 790 sb.append(r.getText()); 791 r.nextTag(); 792 } 793 } else { 794 sb.append('<').append(r.getLocalName()); 795 for (var i = 0; i < r.getAttributeCount(); i++) 796 sb.append(' ').append(r.getAttributeName(i)).append('=').append('\'').append(r.getAttributeValue(i)).append('\''); 797 sb.append('>'); 798 depth++; 799 } 800 } else if (et == END_ELEMENT) { 801 if (nn(characters)) { 802 if (sb.isEmpty()) 803 characters = trimStart(characters); 804 if (depth == 0) 805 characters = trimEnd(characters); 806 sb.append(characters); 807 characters = null; 808 } 809 if (depth == 0) 810 break; 811 sb.append('<').append(r.getLocalName()).append('>'); 812 depth--; 813 } else if (et == CHARACTERS) { 814 characters = r.getText(); 815 } 816 et = r.next(); 817 } 818 819 String s = trim(sb.toString()); 820 returnStringBuilder(sb); 821 return s; 822 } 823 824 @Override /* Overridden from XmlParserSession */ 825 protected String readWhitespaceElement(XmlReader r) throws IOException, ParseException, XMLStreamException { 826 827 var tag = HtmlTag.forEvent(this, r); 828 int et = r.next(); 829 if (tag == BR) { 830 return "\n"; 831 } else if (tag == BS) { 832 return "\b"; 833 } else if (tag == FF) { 834 return "\f"; 835 } else if (tag == SP) { 836 if (et == CHARACTERS) { 837 String s = r.getText(); 838 if (s.charAt(0) == '\u2003') 839 s = "\t"; 840 r.next(); 841 return decodeString(s); 842 } 843 return ""; 844 } else { 845 throw new ParseException(this, "Invalid tag found in readWhitespaceElement(): '%s'", tag); 846 } 847 } 848}