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}