001// *************************************************************************************************************************** 002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * 003// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * 004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance * 005// * with the License. You may obtain a copy of the License at * 006// * * 007// * http://www.apache.org/licenses/LICENSE-2.0 * 008// * * 009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an * 010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * 011// * specific language governing permissions and limitations under the License. * 012// *************************************************************************************************************************** 013package org.apache.juneau.html; 014 015import static javax.xml.stream.XMLStreamConstants.*; 016import static org.apache.juneau.common.internal.StringUtils.*; 017import static org.apache.juneau.html.HtmlTag.*; 018import static org.apache.juneau.internal.CollectionUtils.*; 019 020import java.io.IOException; 021import java.lang.reflect.*; 022import java.nio.charset.*; 023import java.util.*; 024import java.util.function.*; 025 026import javax.xml.stream.*; 027 028import org.apache.juneau.*; 029import org.apache.juneau.collections.*; 030import org.apache.juneau.html.annotation.*; 031import org.apache.juneau.httppart.*; 032import org.apache.juneau.internal.*; 033import org.apache.juneau.parser.*; 034import org.apache.juneau.swap.*; 035import org.apache.juneau.xml.*; 036 037/** 038 * ContextSession object that lives for the duration of a single use of {@link HtmlParser}. 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 * <h5 class='section'>See Also:</h5><ul> 045 * <li class='link'><a class="doclink" href="../../../../index.html#jm.HtmlDetails">HTML Details</a> 046 047 * </ul> 048 */ 049@SuppressWarnings({ "unchecked", "rawtypes" }) 050public final class HtmlParserSession extends XmlParserSession { 051 052 //------------------------------------------------------------------------------------------------------------------- 053 // Static 054 //------------------------------------------------------------------------------------------------------------------- 055 056 private static final Set<String> whitespaceElements = set("br","bs","sp","ff"); 057 058 /** 059 * Creates a new builder for this object. 060 * 061 * @param ctx The context creating this session. 062 * @return A new builder. 063 */ 064 public static Builder create(HtmlParser ctx) { 065 return new Builder(ctx); 066 } 067 068 //------------------------------------------------------------------------------------------------------------------- 069 // Builder 070 //------------------------------------------------------------------------------------------------------------------- 071 072 /** 073 * Builder class. 074 */ 075 @FluentSetters 076 public static class Builder extends XmlParserSession.Builder { 077 078 HtmlParser ctx; 079 080 /** 081 * Constructor 082 * 083 * @param ctx The context creating this session. 084 */ 085 protected Builder(HtmlParser ctx) { 086 super(ctx); 087 this.ctx = ctx; 088 } 089 090 @Override 091 public HtmlParserSession build() { 092 return new HtmlParserSession(this); 093 } 094 095 // <FluentSetters> 096 097 @Override /* GENERATED - org.apache.juneau.ContextSession.Builder */ 098 public <T> Builder apply(Class<T> type, Consumer<T> apply) { 099 super.apply(type, apply); 100 return this; 101 } 102 103 @Override /* GENERATED - org.apache.juneau.ContextSession.Builder */ 104 public Builder debug(Boolean value) { 105 super.debug(value); 106 return this; 107 } 108 109 @Override /* GENERATED - org.apache.juneau.ContextSession.Builder */ 110 public Builder properties(Map<String,Object> value) { 111 super.properties(value); 112 return this; 113 } 114 115 @Override /* GENERATED - org.apache.juneau.ContextSession.Builder */ 116 public Builder property(String key, Object value) { 117 super.property(key, value); 118 return this; 119 } 120 121 @Override /* GENERATED - org.apache.juneau.ContextSession.Builder */ 122 public Builder unmodifiable() { 123 super.unmodifiable(); 124 return this; 125 } 126 127 @Override /* GENERATED - org.apache.juneau.BeanSession.Builder */ 128 public Builder locale(Locale value) { 129 super.locale(value); 130 return this; 131 } 132 133 @Override /* GENERATED - org.apache.juneau.BeanSession.Builder */ 134 public Builder localeDefault(Locale value) { 135 super.localeDefault(value); 136 return this; 137 } 138 139 @Override /* GENERATED - org.apache.juneau.BeanSession.Builder */ 140 public Builder mediaType(MediaType value) { 141 super.mediaType(value); 142 return this; 143 } 144 145 @Override /* GENERATED - org.apache.juneau.BeanSession.Builder */ 146 public Builder mediaTypeDefault(MediaType value) { 147 super.mediaTypeDefault(value); 148 return this; 149 } 150 151 @Override /* GENERATED - org.apache.juneau.BeanSession.Builder */ 152 public Builder timeZone(TimeZone value) { 153 super.timeZone(value); 154 return this; 155 } 156 157 @Override /* GENERATED - org.apache.juneau.BeanSession.Builder */ 158 public Builder timeZoneDefault(TimeZone value) { 159 super.timeZoneDefault(value); 160 return this; 161 } 162 163 @Override /* GENERATED - org.apache.juneau.parser.ParserSession.Builder */ 164 public Builder javaMethod(Method value) { 165 super.javaMethod(value); 166 return this; 167 } 168 169 @Override /* GENERATED - org.apache.juneau.parser.ParserSession.Builder */ 170 public Builder outer(Object value) { 171 super.outer(value); 172 return this; 173 } 174 175 @Override /* GENERATED - org.apache.juneau.parser.ParserSession.Builder */ 176 public Builder schema(HttpPartSchema value) { 177 super.schema(value); 178 return this; 179 } 180 181 @Override /* GENERATED - org.apache.juneau.parser.ParserSession.Builder */ 182 public Builder schemaDefault(HttpPartSchema value) { 183 super.schemaDefault(value); 184 return this; 185 } 186 187 @Override /* GENERATED - org.apache.juneau.parser.ReaderParserSession.Builder */ 188 public Builder fileCharset(Charset value) { 189 super.fileCharset(value); 190 return this; 191 } 192 193 @Override /* GENERATED - org.apache.juneau.parser.ReaderParserSession.Builder */ 194 public Builder streamCharset(Charset value) { 195 super.streamCharset(value); 196 return this; 197 } 198 199 // </FluentSetters> 200 } 201 202 //------------------------------------------------------------------------------------------------------------------- 203 // Instance 204 //------------------------------------------------------------------------------------------------------------------- 205 206 private final HtmlParser ctx; 207 208 /** 209 * Constructor. 210 * 211 * @param builder The builder for this object. 212 */ 213 protected HtmlParserSession(Builder builder) { 214 super(builder); 215 ctx = builder.ctx; 216 } 217 218 @Override /* ParserSession */ 219 protected <T> T doParse(ParserPipe pipe, ClassMeta<T> type) throws IOException, ParseException, ExecutableException { 220 try { 221 return parseAnything(type, getXmlReader(pipe), getOuter(), true, null); 222 } catch (XMLStreamException e) { 223 throw new ParseException(e); 224 } 225 } 226 227 @Override /* ReaderParserSession */ 228 protected <K,V> Map<K,V> doParseIntoMap(ParserPipe pipe, Map<K,V> m, Type keyType, Type valueType) 229 throws Exception { 230 return parseIntoMap(getXmlReader(pipe), m, (ClassMeta<K>)getClassMeta(keyType), 231 (ClassMeta<V>)getClassMeta(valueType), null); 232 } 233 234 @Override /* ReaderParserSession */ 235 protected <E> Collection<E> doParseIntoCollection(ParserPipe pipe, Collection<E> c, Type elementType) 236 throws Exception { 237 return parseIntoCollection(getXmlReader(pipe), c, getClassMeta(elementType), null); 238 } 239 240 /* 241 * Reads anything starting at the current event. 242 * <p> 243 * Precondition: Must be pointing at outer START_ELEMENT. 244 * Postcondition: Pointing at outer END_ELEMENT. 245 */ 246 private <T> T parseAnything(ClassMeta<T> eType, XmlReader r, Object outer, boolean isRoot, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException, XMLStreamException { 247 248 if (eType == null) 249 eType = (ClassMeta<T>)object(); 250 ObjectSwap<T,Object> swap = (ObjectSwap<T,Object>)eType.getSwap(this); 251 BuilderSwap<T,Object> builder = (BuilderSwap<T,Object>)eType.getBuilderSwap(this); 252 ClassMeta<?> sType = null; 253 if (builder != null) 254 sType = builder.getBuilderClassMeta(this); 255 else if (swap != null) 256 sType = swap.getSwapClassMeta(this); 257 else 258 sType = eType; 259 260 if (sType.isOptional()) 261 return (T)optional(parseAnything(eType.getElementType(), r, outer, isRoot, pMeta)); 262 263 setCurrentClass(sType); 264 265 int event = r.getEventType(); 266 if (event != START_ELEMENT) 267 throw new ParseException(this, "parseAnything must be called on outer start element."); 268 269 if (! isRoot) 270 event = r.next(); 271 boolean isEmpty = (event == END_ELEMENT); 272 273 // Skip until we find a start element, end document, or non-empty text. 274 if (! isEmpty) 275 event = skipWs(r); 276 277 if (event == END_DOCUMENT) 278 throw new ParseException(this, "Unexpected end of stream in parseAnything for type ''{0}''", eType); 279 280 // Handle @Html(asXml=true) beans. 281 HtmlClassMeta hcm = getHtmlClassMeta(sType); 282 if (hcm.getFormat() == HtmlFormat.XML) 283 return super.parseAnything(eType, null, r, outer, false, pMeta); 284 285 Object o = null; 286 287 boolean isValid = true; 288 HtmlTag tag = (event == CHARACTERS ? null : HtmlTag.forString(r.getName().getLocalPart(), false)); 289 290 // If it's not a known tag, then parse it as XML. 291 // Allows us to parse stuff like "<div/>" into HTML5 beans. 292 if (tag == null && event != CHARACTERS) 293 return super.parseAnything(eType, null, r, outer, false, pMeta); 294 295 if (tag == HTML) 296 tag = skipToData(r); 297 298 if (isEmpty) { 299 o = ""; 300 } else if (tag == null || tag.isOneOf(BR,BS,FF,SP)) { 301 String text = parseText(r); 302 if (sType.isObject() || sType.isCharSequence()) 303 o = text; 304 else if (sType.isChar()) 305 o = parseCharacter(text); 306 else if (sType.isBoolean()) 307 o = Boolean.parseBoolean(text); 308 else if (sType.isNumber()) 309 o = parseNumber(text, (Class<? extends Number>)eType.getInnerClass()); 310 else if (sType.canCreateNewInstanceFromString(outer)) 311 o = sType.newInstanceFromString(outer, text); 312 else 313 isValid = false; 314 315 } else if (tag == STRING || (tag == A && pMeta != null && getHtmlBeanPropertyMeta(pMeta).getLink() != null)) { 316 String text = getElementText(r); 317 if (sType.isObject() || sType.isCharSequence()) 318 o = text; 319 else if (sType.isChar()) 320 o = parseCharacter(text); 321 else if (sType.canCreateNewInstanceFromString(outer)) 322 o = sType.newInstanceFromString(outer, text); 323 else 324 isValid = false; 325 skipTag(r, tag == STRING ? xSTRING : xA); 326 327 } else if (tag == NUMBER) { 328 String text = getElementText(r); 329 if (sType.isObject()) 330 o = parseNumber(text, Number.class); 331 else if (sType.isNumber()) 332 o = parseNumber(text, (Class<? extends Number>)sType.getInnerClass()); 333 else 334 isValid = false; 335 skipTag(r, xNUMBER); 336 337 } else if (tag == BOOLEAN) { 338 String text = getElementText(r); 339 if (sType.isObject() || sType.isBoolean()) 340 o = Boolean.parseBoolean(text); 341 else 342 isValid = false; 343 skipTag(r, xBOOLEAN); 344 345 } else if (tag == P) { 346 String text = getElementText(r); 347 if (! "No Results".equals(text)) 348 isValid = false; 349 skipTag(r, xP); 350 351 } else if (tag == NULL) { 352 skipTag(r, NULL); 353 skipTag(r, xNULL); 354 355 } else if (tag == A) { 356 o = parseAnchor(r, swap == null ? eType : null); 357 skipTag(r, xA); 358 359 } else if (tag == TABLE) { 360 361 String typeName = getAttribute(r, getBeanTypePropertyName(eType), "object"); 362 ClassMeta cm = getClassMeta(typeName, pMeta, eType); 363 364 if (cm != null) { 365 sType = eType = cm; 366 typeName = sType.isCollectionOrArray() ? "array" : "object"; 367 } else if (! "array".equals(typeName)) { 368 // Type name could be a subtype name. 369 typeName = sType.isCollectionOrArray() ? "array" : "object"; 370 } 371 372 if (typeName.equals("object")) { 373 if (sType.isObject()) { 374 o = parseIntoMap(r, newGenericMap(sType), sType.getKeyType(), sType.getValueType(), 375 pMeta); 376 } else if (sType.isMap()) { 377 o = parseIntoMap(r, (Map)(sType.canCreateNewInstance(outer) ? sType.newInstance(outer) 378 : newGenericMap(sType)), sType.getKeyType(), sType.getValueType(), pMeta); 379 } else if (builder != null) { 380 BeanMap m = toBeanMap(builder.create(this, eType)); 381 o = builder.build(this, parseIntoBean(r, m).getBean(), eType); 382 } else if (sType.canCreateNewBean(outer)) { 383 BeanMap m = newBeanMap(outer, sType.getInnerClass()); 384 o = parseIntoBean(r, m).getBean(); 385 } else if (sType.getProxyInvocationHandler() != null) { 386 BeanMap m = newBeanMap(outer, sType.getInnerClass()); 387 o = parseIntoBean(r, m).getBean(); 388 } else { 389 isValid = false; 390 } 391 skipTag(r, xTABLE); 392 393 } else if (typeName.equals("array")) { 394 if (sType.isObject()) 395 o = parseTableIntoCollection(r, (Collection)new JsonList(this), sType, pMeta); 396 else if (sType.isCollection()) 397 o = parseTableIntoCollection(r, (Collection)(sType.canCreateNewInstance(outer) 398 ? sType.newInstance(outer) : new JsonList(this)), sType, pMeta); 399 else if (sType.isArray() || sType.isArgs()) { 400 ArrayList l = (ArrayList)parseTableIntoCollection(r, list(), sType, pMeta); 401 o = toArray(sType, l); 402 } 403 else 404 isValid = false; 405 skipTag(r, xTABLE); 406 407 } else { 408 isValid = false; 409 } 410 411 } else if (tag == UL) { 412 String typeName = getAttribute(r, getBeanTypePropertyName(eType), "array"); 413 ClassMeta cm = getClassMeta(typeName, pMeta, eType); 414 if (cm != null) 415 sType = eType = cm; 416 417 if (sType.isObject()) 418 o = parseIntoCollection(r, new JsonList(this), sType, pMeta); 419 else if (sType.isCollection() || sType.isObject()) 420 o = parseIntoCollection(r, (Collection)(sType.canCreateNewInstance(outer) 421 ? sType.newInstance(outer) : new JsonList(this)), sType, pMeta); 422 else if (sType.isArray() || sType.isArgs()) 423 o = toArray(sType, parseIntoCollection(r, list(), sType, pMeta)); 424 else 425 isValid = false; 426 skipTag(r, xUL); 427 428 } 429 430 if (! isValid) 431 throw new ParseException(this, "Unexpected tag ''{0}'' for type ''{1}''", tag, eType); 432 433 if (swap != null && o != null) 434 o = unswap(swap, o, eType); 435 436 if (outer != null) 437 setParent(eType, o, outer); 438 439 skipWs(r); 440 return (T)o; 441 } 442 443 /* 444 * For parsing output from HtmlDocSerializer, this skips over the head, title, and links. 445 */ 446 private HtmlTag skipToData(XmlReader r) throws ParseException, XMLStreamException { 447 while (true) { 448 int event = r.next(); 449 if (event == START_ELEMENT && "div".equals(r.getLocalName()) && "data".equals(r.getAttributeValue(null, "id"))) { 450 r.nextTag(); 451 event = r.getEventType(); 452 boolean isEmpty = (event == END_ELEMENT); 453 // Skip until we find a start element, end document, or non-empty text. 454 if (! isEmpty) 455 event = skipWs(r); 456 if (event == END_DOCUMENT) 457 throw new ParseException(this, "Unexpected end of stream looking for data."); 458 return (event == CHARACTERS ? null : HtmlTag.forString(r.getName().getLocalPart(), false)); 459 } 460 } 461 } 462 463 private static String getAttribute(XmlReader r, String name, String def) { 464 for (int i = 0; i < r.getAttributeCount(); i++) 465 if (r.getAttributeLocalName(i).equals(name)) 466 return r.getAttributeValue(i); 467 return def; 468 } 469 470 /* 471 * Reads an anchor tag and converts it into a bean. 472 */ 473 private <T> T parseAnchor(XmlReader r, ClassMeta<T> beanType) 474 throws IOException, ParseException, XMLStreamException { 475 String href = r.getAttributeValue(null, "href"); 476 String name = getElementText(r); 477 if (beanType != null && beanType.hasAnnotation(HtmlLink.class)) { 478 Value<String> uriProperty = Value.empty(), nameProperty = Value.empty(); 479 beanType.forEachAnnotation(HtmlLink.class, x -> isNotEmpty(x.uriProperty()), x -> uriProperty.set(x.uriProperty())); 480 beanType.forEachAnnotation(HtmlLink.class, x -> isNotEmpty(x.nameProperty()), x -> nameProperty.set(x.nameProperty())); 481 BeanMap<T> m = newBeanMap(beanType.getInnerClass()); 482 m.put(uriProperty.orElse(""), href); 483 m.put(nameProperty.orElse(""), name); 484 return m.getBean(); 485 } 486 return convertToType(href, beanType); 487 } 488 489 private static Map<String,String> getAttributes(XmlReader r) { 490 Map<String,String> m = new TreeMap<>() ; 491 for (int i = 0; i < r.getAttributeCount(); i++) 492 m.put(r.getAttributeLocalName(i), r.getAttributeValue(i)); 493 return m; 494 } 495 496 /* 497 * Reads contents of <table> element. 498 * Precondition: Must be pointing at <table> event. 499 * Postcondition: Pointing at next START_ELEMENT or END_DOCUMENT event. 500 */ 501 private <K,V> Map<K,V> parseIntoMap(XmlReader r, Map<K,V> m, ClassMeta<K> keyType, 502 ClassMeta<V> valueType, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException, XMLStreamException { 503 while (true) { 504 HtmlTag tag = nextTag(r, TR, xTABLE); 505 if (tag == xTABLE) 506 break; 507 tag = nextTag(r, TD, TH); 508 // Skip over the column headers. 509 if (tag == TH) { 510 skipTag(r); 511 r.nextTag(); 512 skipTag(r); 513 } else { 514 K key = parseAnything(keyType, r, m, false, pMeta); 515 nextTag(r, TD); 516 V value = parseAnything(valueType, r, m, false, pMeta); 517 setName(valueType, value, key); 518 m.put(key, value); 519 } 520 tag = nextTag(r, xTD, xTR); 521 if (tag == xTD) 522 nextTag(r, xTR); 523 } 524 525 return m; 526 } 527 528 /* 529 * Reads contents of <ul> element. 530 * Precondition: Must be pointing at event following <ul> event. 531 * Postcondition: Pointing at next START_ELEMENT or END_DOCUMENT event. 532 */ 533 private <E> Collection<E> parseIntoCollection(XmlReader r, Collection<E> l, 534 ClassMeta<?> type, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException, XMLStreamException { 535 int argIndex = 0; 536 while (true) { 537 HtmlTag tag = nextTag(r, LI, xUL, xLI); 538 if (tag == xLI) 539 tag = nextTag(r, LI, xUL, xLI); 540 if (tag == xUL) 541 break; 542 ClassMeta<?> elementType = type.isArgs() ? type.getArg(argIndex++) : type.getElementType(); 543 l.add((E)parseAnything(elementType, r, l, false, pMeta)); 544 } 545 return l; 546 } 547 548 /* 549 * Reads contents of <ul> element. 550 * Precondition: Must be pointing at event following <ul> event. 551 * Postcondition: Pointing at next START_ELEMENT or END_DOCUMENT event. 552 */ 553 private <E> Collection<E> parseTableIntoCollection(XmlReader r, Collection<E> l, 554 ClassMeta<E> type, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException, XMLStreamException { 555 556 HtmlTag tag = nextTag(r, TR); 557 List<String> keys = list(); 558 while (true) { 559 tag = nextTag(r, TH, xTR); 560 if (tag == xTR) 561 break; 562 keys.add(getElementText(r)); 563 } 564 565 int argIndex = 0; 566 567 while (true) { 568 r.nextTag(); 569 tag = HtmlTag.forEvent(this, r); 570 if (tag == xTABLE) 571 break; 572 573 ClassMeta elementType = null; 574 String beanType = getAttribute(r, getBeanTypePropertyName(type), null); 575 if (beanType != null) 576 elementType = getClassMeta(beanType, pMeta, null); 577 if (elementType == null) 578 elementType = type.isArgs() ? type.getArg(argIndex++) : type.getElementType(); 579 if (elementType == null) 580 elementType = object(); 581 582 BuilderSwap<E,Object> builder = elementType.getBuilderSwap(this); 583 584 if (builder != null || elementType.canCreateNewBean(l)) { 585 BeanMap m = 586 builder != null 587 ? toBeanMap(builder.create(this, elementType)) 588 : newBeanMap(l, elementType.getInnerClass()) 589 ; 590 for (String key : keys) { 591 tag = nextTag(r, xTD, TD, NULL); 592 if (tag == xTD) 593 tag = nextTag(r, TD, NULL); 594 if (tag == NULL) { 595 m = null; 596 nextTag(r, xNULL); 597 break; 598 } 599 BeanMapEntry e = m.getProperty(key); 600 if (e == null) { 601 //onUnknownProperty(key, m, -1, -1); 602 parseAnything(object(), r, l, false, null); 603 } else { 604 BeanPropertyMeta bpm = e.getMeta(); 605 ClassMeta<?> cm = bpm.getClassMeta(); 606 Object value = parseAnything(cm, r, m.getBean(false), false, bpm); 607 setName(cm, value, key); 608 bpm.set(m, key, value); 609 } 610 } 611 l.add( 612 m == null 613 ? null 614 : builder != null 615 ? builder.build(this, m.getBean(), elementType) 616 : (E)m.getBean() 617 ); 618 } else { 619 String c = getAttributes(r).get(getBeanTypePropertyName(type.getElementType())); 620 Map m = (Map)(elementType.isMap() && elementType.canCreateNewInstance(l) ? elementType.newInstance(l) 621 : newGenericMap(elementType)); 622 for (String key : keys) { 623 tag = nextTag(r, TD, NULL); 624 if (tag == NULL) { 625 m = null; 626 nextTag(r, xNULL); 627 break; 628 } 629 if (m != null) { 630 ClassMeta<?> kt = elementType.getKeyType(), vt = elementType.getValueType(); 631 Object value = parseAnything(vt, r, l, false, pMeta); 632 setName(vt, value, key); 633 m.put(convertToType(key, kt), value); 634 } 635 } 636 if (m != null && c != null) { 637 JsonMap m2 = (m instanceof JsonMap ? (JsonMap)m : new JsonMap(m).session(this)); 638 m2.put(getBeanTypePropertyName(type.getElementType()), c); 639 l.add((E)cast(m2, pMeta, elementType)); 640 } else { 641 if (m instanceof JsonMap) 642 l.add((E)convertToType(m, elementType)); 643 else 644 l.add((E)m); 645 } 646 } 647 nextTag(r, xTR); 648 } 649 return l; 650 } 651 652 /* 653 * Reads contents of <table> element. 654 * Precondition: Must be pointing at event following <table> event. 655 * Postcondition: Pointing at next START_ELEMENT or END_DOCUMENT event. 656 */ 657 private <T> BeanMap<T> parseIntoBean(XmlReader r, BeanMap<T> m) throws IOException, ParseException, ExecutableException, XMLStreamException { 658 while (true) { 659 HtmlTag tag = nextTag(r, TR, xTABLE); 660 if (tag == xTABLE) 661 break; 662 tag = nextTag(r, TD, TH); 663 // Skip over the column headers. 664 if (tag == TH) { 665 skipTag(r); 666 r.nextTag(); 667 skipTag(r); 668 } else { 669 String key = getElementText(r); 670 nextTag(r, TD); 671 BeanPropertyMeta pMeta = m.getPropertyMeta(key); 672 if (pMeta == null) { 673 onUnknownProperty(key, m, parseAnything(object(), r, null, false, null)); 674 } else { 675 ClassMeta<?> cm = pMeta.getClassMeta(); 676 Object value = parseAnything(cm, r, m.getBean(false), false, pMeta); 677 setName(cm, value, key); 678 try { 679 pMeta.set(m, key, value); 680 } catch (BeanRuntimeException e) { 681 onBeanSetterException(pMeta, e); 682 throw e; 683 } 684 } 685 } 686 HtmlTag t = nextTag(r, xTD, xTR); 687 if (t == xTD) 688 nextTag(r, xTR); 689 } 690 return m; 691 } 692 693 /* 694 * Reads the next tag. Advances past anything that's not a start or end tag. Throws an exception if 695 * it's not one of the expected tags. 696 * Precondition: Must be pointing before the event we want to parse. 697 * Postcondition: Pointing at the tag just parsed. 698 */ 699 private HtmlTag nextTag(XmlReader r, HtmlTag...expected) throws ParseException, XMLStreamException { 700 int et = r.next(); 701 702 while (et != START_ELEMENT && et != END_ELEMENT && et != END_DOCUMENT) 703 et = r.next(); 704 705 if (et == END_DOCUMENT) 706 throw new ParseException(this, "Unexpected end of document."); 707 708 HtmlTag tag = HtmlTag.forEvent(this, r); 709 if (expected.length == 0) 710 return tag; 711 for (HtmlTag t : expected) 712 if (t == tag) 713 return tag; 714 715 throw new ParseException(this, "Unexpected tag: ''{0}''. Expected one of the following: {1}", tag, expected); 716 } 717 718 /* 719 * Skips over the current element and advances to the next element. 720 * <p> 721 * Precondition: Pointing to opening tag. 722 * Postcondition: Pointing to next opening tag. 723 * 724 * @param r The stream being read from. 725 * @throws XMLStreamException 726 */ 727 private void skipTag(XmlReader r) throws ParseException, XMLStreamException { 728 int et = r.getEventType(); 729 730 if (et != START_ELEMENT) 731 throw new ParseException(this, 732 "skipToNextTag() call on invalid event ''{0}''. Must only be called on START_ELEMENT events.", 733 XmlUtils.toReadableEvent(r) 734 ); 735 736 String n = r.getLocalName(); 737 738 int depth = 0; 739 while (true) { 740 et = r.next(); 741 if (et == START_ELEMENT) { 742 String n2 = r.getLocalName(); 743 if (n.equals(n2)) 744 depth++; 745 } else if (et == END_ELEMENT) { 746 String n2 = r.getLocalName(); 747 if (n.equals(n2)) 748 depth--; 749 if (depth < 0) 750 return; 751 } 752 } 753 } 754 755 private void skipTag(XmlReader r, HtmlTag...expected) throws ParseException, XMLStreamException { 756 HtmlTag tag = HtmlTag.forEvent(this, r); 757 if (tag.isOneOf(expected)) 758 r.next(); 759 else 760 throw new ParseException(this, 761 "Unexpected tag: ''{0}''. Expected one of the following: {1}", 762 tag, expected); 763 } 764 765 private static int skipWs(XmlReader r) throws XMLStreamException { 766 int event = r.getEventType(); 767 while (event != START_ELEMENT && event != END_ELEMENT && event != END_DOCUMENT && r.isWhiteSpace()) 768 event = r.next(); 769 return event; 770 } 771 772 /** 773 * Parses CHARACTERS data. 774 * 775 * <p> 776 * Precondition: Pointing to event immediately following opening tag. 777 * Postcondition: Pointing to closing tag. 778 * 779 * @param r The stream being read from. 780 * @return The parsed string. 781 * @throws XMLStreamException Thrown by underlying XML stream. 782 */ 783 @Override /* XmlParserSession */ 784 protected String parseText(XmlReader r) throws IOException, ParseException, XMLStreamException { 785 786 StringBuilder sb = getStringBuilder(); 787 788 int et = r.getEventType(); 789 if (et == END_ELEMENT) 790 return ""; 791 792 int depth = 0; 793 794 String characters = null; 795 796 while (true) { 797 if (et == START_ELEMENT) { 798 if (characters != null) { 799 if (sb.length() == 0) 800 characters = trimStart(characters); 801 sb.append(characters); 802 characters = null; 803 } 804 HtmlTag tag = HtmlTag.forEvent(this, r); 805 if (tag == BR) { 806 sb.append('\n'); 807 r.nextTag(); 808 } else if (tag == BS) { 809 sb.append('\b'); 810 r.nextTag(); 811 } else if (tag == SP) { 812 et = r.next(); 813 if (et == CHARACTERS) { 814 String s = r.getText(); 815 if (s.length() > 0) { 816 char c = r.getText().charAt(0); 817 if (c == '\u2003') 818 c = '\t'; 819 sb.append(c); 820 } 821 r.nextTag(); 822 } 823 } else if (tag == FF) { 824 sb.append('\f'); 825 r.nextTag(); 826 } else if (tag.isOneOf(STRING, NUMBER, BOOLEAN)) { 827 et = r.next(); 828 if (et == CHARACTERS) { 829 sb.append(r.getText()); 830 r.nextTag(); 831 } 832 } else { 833 sb.append('<').append(r.getLocalName()); 834 for (int i = 0; i < r.getAttributeCount(); i++) 835 sb.append(' ').append(r.getAttributeName(i)).append('=').append('\'').append(r.getAttributeValue(i)).append('\''); 836 sb.append('>'); 837 depth++; 838 } 839 } else if (et == END_ELEMENT) { 840 if (characters != null) { 841 if (sb.length() == 0) 842 characters = trimStart(characters); 843 if (depth == 0) 844 characters = trimEnd(characters); 845 sb.append(characters); 846 characters = null; 847 } 848 if (depth == 0) 849 break; 850 sb.append('<').append(r.getLocalName()).append('>'); 851 depth--; 852 } else if (et == CHARACTERS) { 853 characters = r.getText(); 854 } 855 et = r.next(); 856 } 857 858 String s = trim(sb.toString()); 859 returnStringBuilder(sb); 860 return s; 861 } 862 863 /** 864 * Identical to {@link #parseText(XmlReader)} except assumes the current event is the opening tag. 865 * 866 * <p> 867 * Precondition: Pointing to opening tag. 868 * Postcondition: Pointing to closing tag. 869 * 870 * @param r The stream being read from. 871 * @return The parsed string. 872 * @throws XMLStreamException Thrown by underlying XML stream. 873 * @throws ParseException Malformed input encountered. 874 */ 875 @Override /* XmlParserSession */ 876 protected String getElementText(XmlReader r) throws IOException, XMLStreamException, ParseException { 877 r.next(); 878 return parseText(r); 879 } 880 881 @Override /* XmlParserSession */ 882 protected boolean isWhitespaceElement(XmlReader r) { 883 String s = r.getLocalName(); 884 return whitespaceElements.contains(s); 885 } 886 887 @Override /* XmlParserSession */ 888 protected String parseWhitespaceElement(XmlReader r) throws IOException, ParseException, XMLStreamException { 889 890 HtmlTag tag = HtmlTag.forEvent(this, r); 891 int et = r.next(); 892 if (tag == BR) { 893 return "\n"; 894 } else if (tag == BS) { 895 return "\b"; 896 } else if (tag == FF) { 897 return "\f"; 898 } else if (tag == SP) { 899 if (et == CHARACTERS) { 900 String s = r.getText(); 901 if (s.charAt(0) == '\u2003') 902 s = "\t"; 903 r.next(); 904 return decodeString(s); 905 } 906 return ""; 907 } else { 908 throw new ParseException(this, "Invalid tag found in parseWhitespaceElement(): ''{0}''", tag); 909 } 910 } 911 912 //----------------------------------------------------------------------------------------------------------------- 913 // Extended metadata 914 //----------------------------------------------------------------------------------------------------------------- 915 916 /** 917 * Returns the language-specific metadata on the specified class. 918 * 919 * @param cm The class to return the metadata on. 920 * @return The metadata. 921 */ 922 protected HtmlClassMeta getHtmlClassMeta(ClassMeta<?> cm) { 923 return ctx.getHtmlClassMeta(cm); 924 } 925 926 /** 927 * Returns the language-specific metadata on the specified bean property. 928 * 929 * @param bpm The bean property to return the metadata on. 930 * @return The metadata. 931 */ 932 protected HtmlBeanPropertyMeta getHtmlBeanPropertyMeta(BeanPropertyMeta bpm) { 933 return ctx.getHtmlBeanPropertyMeta(bpm); 934 } 935}