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.xml;
014
015import static javax.xml.stream.XMLStreamConstants.*;
016import static org.apache.juneau.internal.StringUtils.*;
017import static org.apache.juneau.xml.annotation.XmlFormat.*;
018
019import java.io.IOException;
020import java.lang.reflect.*;
021import java.util.*;
022
023import javax.xml.stream.*;
024import javax.xml.stream.util.*;
025
026import org.apache.juneau.*;
027import org.apache.juneau.parser.*;
028import org.apache.juneau.transform.*;
029import org.apache.juneau.xml.annotation.*;
030
031/**
032 * Session object that lives for the duration of a single use of {@link XmlParser}.
033 *
034 * <p>
035 * This class is NOT thread safe.
036 * It is typically discarded after one-time use although it can be reused against multiple inputs.
037 */
038@SuppressWarnings({ "unchecked", "rawtypes" })
039public class XmlParserSession extends ReaderParserSession {
040
041   private static final int UNKNOWN=0, OBJECT=1, ARRAY=2, STRING=3, NUMBER=4, BOOLEAN=5, NULL=6;
042
043   private final XmlParser ctx;
044   private final StringBuilder rsb = new StringBuilder();  // Reusable string builder used in this class.
045
046   /**
047    * Create a new session using properties specified in the context.
048    *
049    * @param ctx
050    *    The context creating this session object.
051    *    The context contains all the configuration settings for this object.
052    * @param args
053    *    Runtime session arguments.
054    */
055   protected XmlParserSession(XmlParser ctx, ParserSessionArgs args) {
056      super(ctx, args);
057      this.ctx = ctx;
058   }
059
060   /**
061    * Wrap the specified reader in a STAX reader based on settings in this context.
062    *
063    * @param pipe The parser input.
064    * @return The new STAX reader.
065    * @throws IOException Thrown by underlying stream.
066    * @throws XMLStreamException Unexpected XML processing error.
067    */
068   protected final XmlReader getXmlReader(ParserPipe pipe) throws IOException, XMLStreamException {
069      return new XmlReader(pipe, isValidating(), getReporter(), getResolver(), getEventAllocator());
070   }
071
072   /**
073    * Decodes and trims the specified string.
074    *
075    * <p>
076    * Any <js>'_x####_'</js> sequences in the string will be decoded.
077    *
078    * @param s The string to be decoded.
079    * @return The decoded string.
080    */
081   protected final String decodeString(String s) {
082      if (s == null)
083         return null;
084      rsb.setLength(0);
085      s = XmlUtils.decode(s, rsb);
086      if (isTrimStrings())
087         s = s.trim();
088      return s;
089   }
090
091   /*
092    * Returns the name of the current XML element.
093    * Any <js>'_x####_'</js> sequences in the string will be decoded.
094    */
095   private String getElementName(XmlReader r) {
096      return decodeString(r.getLocalName());
097   }
098
099   /*
100    * Returns the name of the specified attribute on the current XML element.
101    * Any <js>'_x####_'</js> sequences in the string will be decoded.
102    */
103   private String getAttributeName(XmlReader r, int i) {
104      return decodeString(r.getAttributeLocalName(i));
105   }
106
107   /*
108    * Returns the value of the specified attribute on the current XML element.
109    * Any <js>'_x####_'</js> sequences in the string will be decoded.
110    */
111   private String getAttributeValue(XmlReader r, int i) {
112      return decodeString(r.getAttributeValue(i));
113   }
114
115   /**
116    * Returns the text content of the current XML element.
117    *
118    * <p>
119    * Any <js>'_x####_'</js> sequences in the string will be decoded.
120    *
121    * <p>
122    * Leading and trailing whitespace (unencoded) will be trimmed from the result.
123    *
124    * @param r The reader to read the element text from.
125    * @return The decoded text.  <jk>null</jk> if the text consists of the sequence <js>'_x0000_'</js>.
126    * @throws XMLStreamException Thrown by underlying reader.
127    * @throws IOException Thrown by underlying stream.
128    * @throws ParseException Malformed input encountered.
129    */
130   protected String getElementText(XmlReader r) throws XMLStreamException, IOException, ParseException {
131      return decodeString(r.getElementText().trim());
132   }
133
134   /*
135    * Returns the content of the current CHARACTERS node.
136    * Any <js>'_x####_'</js> sequences in the string will be decoded.
137    * Leading and trailing whitespace (unencoded) will be trimmed from the result.
138    */
139   private String getText(XmlReader r, boolean trim) {
140      String s = r.getText();
141      if (trim)
142         s = s.trim();
143      if (s.isEmpty())
144         return null;
145      return decodeString(s);
146   }
147
148   /*
149    * Shortcut for calling <code>getText(r, <jk>true</jk>);</code>.
150    */
151   private String getText(XmlReader r) {
152      return getText(r, true);
153   }
154
155   /*
156    * Takes the element being read from the XML stream reader and reconstructs it as XML.
157    * Used when reconstructing bean properties of type {@link XmlFormat#XMLTEXT}.
158    */
159   private String getElementAsString(XmlReader r) {
160      int t = r.getEventType();
161      if (t > 2)
162         throw new FormattedRuntimeException("Invalid event type on stream reader for elementToString() method: ''{0}''", XmlUtils.toReadableEvent(r));
163      rsb.setLength(0);
164      rsb.append("<").append(t == 1 ? "" : "/").append(r.getLocalName());
165      if (t == 1)
166         for (int i = 0; i < r.getAttributeCount(); i++)
167            rsb.append(' ').append(r.getAttributeName(i)).append('=').append('\'').append(r.getAttributeValue(i)).append('\'');
168      rsb.append('>');
169      return rsb.toString();
170   }
171
172   /**
173    * Parses the current element as text.
174    *
175    * @param r The input reader.
176    * @return The parsed text.
177    * @throws XMLStreamException Thrown by underlying reader.
178    * @throws IOException Thrown by underlying stream.
179    * @throws ParseException Malformed input encountered.
180    */
181   protected String parseText(XmlReader r) throws IOException, XMLStreamException, ParseException {
182      // Note that this is different than {@link #getText(XmlReader)} since it assumes that we're pointing to a
183      // whitespace element.
184
185      StringBuilder sb2 = getStringBuilder();
186
187      int depth = 0;
188      while (true) {
189         int et = r.getEventType();
190         if (et == START_ELEMENT) {
191            sb2.append(getElementAsString(r));
192            depth++;
193         } else if (et == CHARACTERS) {
194            sb2.append(getText(r));
195         } else if (et == END_ELEMENT) {
196            sb2.append(getElementAsString(r));
197            depth--;
198            if (depth <= 0)
199               break;
200         }
201         et = r.next();
202      }
203      String s = sb2.toString();
204      returnStringBuilder(sb2);
205      return s;
206   }
207
208   /**
209    * Returns <jk>true</jk> if the current element is a whitespace element.
210    *
211    * <p>
212    * For the XML parser, this always returns <jk>false</jk>.
213    * However, the HTML parser defines various whitespace elements such as <js>"br"</js> and <js>"sp"</js>.
214    *
215    * @param r The XML stream reader to read the current event from.
216    * @return <jk>true</jk> if the current element is a whitespace element.
217    */
218   protected boolean isWhitespaceElement(XmlReader r) {
219      return false;
220   }
221
222   /**
223    * Parses the current whitespace element.
224    *
225    * <p>
226    * For the XML parser, this always returns <jk>null</jk> since there is no concept of a whitespace element.
227    * However, the HTML parser defines various whitespace elements such as <js>"br"</js> and <js>"sp"</js>.
228    *
229    * @param r The XML stream reader to read the current event from.
230    * @return The whitespace character or characters.
231    * @throws XMLStreamException Thrown by underlying reader.
232    * @throws IOException Thrown by underlying stream.
233    * @throws ParseException Malformed input encountered.
234    */
235   protected String parseWhitespaceElement(XmlReader r) throws IOException, XMLStreamException, ParseException {
236      return null;
237   }
238
239   @Override /* ParserSession */
240   protected <T> T doParse(ParserPipe pipe, ClassMeta<T> type) throws IOException, ParseException, ExecutableException {
241      try {
242         return parseAnything(type, null, getXmlReader(pipe), getOuter(), true, null);
243      } catch (XMLStreamException e) {
244         throw new ParseException(e);
245      }
246   }
247
248   @Override /* ReaderParserSession */
249   protected <K,V> Map<K,V> doParseIntoMap(ParserPipe pipe, Map<K,V> m, Type keyType, Type valueType) throws Exception {
250      ClassMeta cm = getClassMeta(m.getClass(), keyType, valueType);
251      return parseIntoMap(pipe, m, cm.getKeyType(), cm.getValueType());
252   }
253
254   @Override /* ReaderParserSession */
255   protected <E> Collection<E> doParseIntoCollection(ParserPipe pipe, Collection<E> c, Type elementType) throws Exception {
256      ClassMeta cm = getClassMeta(c.getClass(), elementType);
257      return parseIntoCollection(pipe, c, cm.getElementType());
258   }
259
260   /**
261    * Workhorse method.
262    *
263    * @param <T> The expected type of object.
264    * @param eType The expected type of object.
265    * @param currAttr The current bean property name.
266    * @param r The reader.
267    * @param outer The outer object.
268    * @param isRoot If <jk>true</jk>, then we're serializing a root element in the document.
269    * @param pMeta The bean property metadata.
270    * @return The parsed object.
271    * @throws IOException Thrown by underlying stream.
272    * @throws ParseException Malformed input encountered.
273    * @throws ExecutableException Exception occurred on invoked constructor/method/field.
274    * @throws XMLStreamException Malformed XML encountered.
275    */
276   protected <T> T parseAnything(ClassMeta<T> eType, String currAttr, XmlReader r,
277         Object outer, boolean isRoot, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException, XMLStreamException {
278
279      if (eType == null)
280         eType = (ClassMeta<T>)object();
281      PojoSwap<T,Object> swap = (PojoSwap<T,Object>)eType.getPojoSwap(this);
282      BuilderSwap<T,Object> builder = (BuilderSwap<T,Object>)eType.getBuilderSwap(this);
283      ClassMeta<?> sType = null;
284      if (builder != null)
285         sType = builder.getBuilderClassMeta(this);
286      else if (swap != null)
287         sType = swap.getSwapClassMeta(this);
288      else
289         sType = eType;
290      setCurrentClass(sType);
291
292      String wrapperAttr = (isRoot && isPreserveRootElement()) ? r.getName().getLocalPart() : null;
293      String typeAttr = r.getAttributeValue(null, getBeanTypePropertyName(eType));
294      int jsonType = getJsonType(typeAttr);
295      String elementName = getElementName(r);
296      if (jsonType == 0) {
297         if (elementName == null || elementName.equals(currAttr))
298            jsonType = UNKNOWN;
299         else {
300            typeAttr = elementName;
301            jsonType = getJsonType(elementName);
302         }
303      }
304
305      ClassMeta tcm = getClassMeta(typeAttr, pMeta, eType);
306      if (tcm == null && elementName != null && ! elementName.equals(currAttr))
307         tcm = getClassMeta(elementName, pMeta, eType);
308      if (tcm != null)
309         sType = eType = tcm;
310
311      Object o = null;
312
313      if (jsonType == NULL) {
314         r.nextTag();   // Discard end tag
315         return null;
316      }
317
318      if (sType.isObject()) {
319         if (jsonType == OBJECT) {
320            ObjectMap m = new ObjectMap(this);
321            parseIntoMap(r, m, string(), object(), pMeta);
322            if (wrapperAttr != null)
323               m = new ObjectMap(this).append(wrapperAttr, m);
324            o = cast(m, pMeta, eType);
325         } else if (jsonType == ARRAY)
326            o = parseIntoCollection(r, new ObjectList(this), null, pMeta);
327         else if (jsonType == STRING) {
328            o = getElementText(r);
329            if (sType.isChar())
330               o = parseCharacter(o);
331         }
332         else if (jsonType == NUMBER)
333            o = parseNumber(getElementText(r), null);
334         else if (jsonType == BOOLEAN)
335            o = Boolean.parseBoolean(getElementText(r));
336         else if (jsonType == UNKNOWN)
337            o = getUnknown(r);
338      } else if (sType.isBoolean()) {
339         o = Boolean.parseBoolean(getElementText(r));
340      } else if (sType.isCharSequence()) {
341         o = getElementText(r);
342      } else if (sType.isChar()) {
343         o = parseCharacter(getElementText(r));
344      } else if (sType.isMap()) {
345         Map m = (sType.canCreateNewInstance(outer) ? (Map)sType.newInstance(outer) : new ObjectMap(this));
346         o = parseIntoMap(r, m, sType.getKeyType(), sType.getValueType(), pMeta);
347         if (wrapperAttr != null)
348            o = new ObjectMap(this).append(wrapperAttr, m);
349      } else if (sType.isCollection()) {
350         Collection l = (sType.canCreateNewInstance(outer) ? (Collection)sType.newInstance(outer) : new ObjectList(this));
351         o = parseIntoCollection(r, l, sType, pMeta);
352      } else if (sType.isNumber()) {
353         o = parseNumber(getElementText(r), (Class<? extends Number>)sType.getInnerClass());
354      } else if (builder != null || sType.canCreateNewBean(outer)) {
355         if (sType.getExtendedMeta(XmlClassMeta.class).getFormat() == COLLAPSED) {
356            String fieldName = r.getLocalName();
357            BeanMap<?> m = builder != null ? toBeanMap(builder.create(this, eType)) : newBeanMap(outer, sType.getInnerClass());
358            BeanPropertyMeta bpm = m.getMeta().getExtendedMeta(XmlBeanMeta.class).getPropertyMeta(fieldName);
359            ClassMeta<?> cm = m.getMeta().getClassMeta();
360            Object value = parseAnything(cm, currAttr, r, m.getBean(false), false, null);
361            setName(cm, value, currAttr);
362            bpm.set(m, currAttr, value);
363            o = builder != null ? builder.build(this, m.getBean(), eType) : m.getBean();
364         } else {
365            BeanMap m = builder != null ? toBeanMap(builder.create(this, eType)) : newBeanMap(outer, sType.getInnerClass());
366            m = parseIntoBean(r, m);
367            o = builder != null ? builder.build(this, m.getBean(), eType) : m.getBean();
368         }
369      } else if (sType.isArray() || sType.isArgs()) {
370         ArrayList l = (ArrayList)parseIntoCollection(r, new ArrayList(), sType, pMeta);
371         o = toArray(sType, l);
372      } else if (sType.canCreateNewInstanceFromString(outer)) {
373         o = sType.newInstanceFromString(outer, getElementText(r));
374      } else {
375         throw new ParseException(this,
376            "Class ''{0}'' could not be instantiated.  Reason: ''{1}'', property: ''{2}''",
377            sType.getInnerClass().getName(), sType.getNotABeanReason(), pMeta == null ? null : pMeta.getName());
378      }
379
380      if (swap != null && o != null)
381         o = unswap(swap, o, eType);
382
383      if (outer != null)
384         setParent(eType, o, outer);
385
386      return (T)o;
387   }
388
389   private <K,V> Map<K,V> parseIntoMap(XmlReader r, Map<K,V> m, ClassMeta<K> keyType,
390         ClassMeta<V> valueType, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException, XMLStreamException {
391      int depth = 0;
392      for (int i = 0; i < r.getAttributeCount(); i++) {
393         String a = r.getAttributeLocalName(i);
394         // TODO - Need better handling of namespaces here.
395         if (! (a.equals(getBeanTypePropertyName(null)))) {
396            K key = trim(convertAttrToType(m, a, keyType));
397            V value = trim(convertAttrToType(m, r.getAttributeValue(i), valueType));
398            setName(valueType, value, key);
399            m.put(key, value);
400         }
401      }
402      do {
403         int event = r.nextTag();
404         String currAttr;
405         if (event == START_ELEMENT) {
406            depth++;
407            currAttr = getElementName(r);
408            K key = convertAttrToType(m, currAttr, keyType);
409            V value = parseAnything(valueType, currAttr, r, m, false, pMeta);
410            setName(valueType, value, currAttr);
411            if (valueType.isObject() && m.containsKey(key)) {
412               Object o = m.get(key);
413               if (o instanceof List)
414                  ((List)o).add(value);
415               else
416                  m.put(key, (V)new ObjectList(o, value).setBeanSession(this));
417            } else {
418               m.put(key, value);
419            }
420         } else if (event == END_ELEMENT) {
421            depth--;
422            return m;
423         }
424      } while (depth > 0);
425      return m;
426   }
427
428   private <E> Collection<E> parseIntoCollection(XmlReader r, Collection<E> l,
429         ClassMeta<?> type, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException, XMLStreamException {
430      int depth = 0;
431      int argIndex = 0;
432      do {
433         int event = r.nextTag();
434         if (event == START_ELEMENT) {
435            depth++;
436            ClassMeta<?> elementType = type == null ? object() : type.isArgs() ? type.getArg(argIndex++) : type.getElementType();
437            E value = (E)parseAnything(elementType, null, r, l, false, pMeta);
438            l.add(value);
439         } else if (event == END_ELEMENT) {
440            depth--;
441            return l;
442         }
443      } while (depth > 0);
444      return l;
445   }
446
447   private static int getJsonType(String s) {
448      if (s == null)
449         return UNKNOWN;
450      char c = s.charAt(0);
451      switch(c) {
452         case 'o': return (s.equals("object") ? OBJECT : UNKNOWN);
453         case 'a': return (s.equals("array") ? ARRAY : UNKNOWN);
454         case 's': return (s.equals("string") ? STRING : UNKNOWN);
455         case 'b': return (s.equals("boolean") ? BOOLEAN : UNKNOWN);
456         case 'n': {
457            c = s.charAt(2);
458            switch(c) {
459               case 'm': return (s.equals("number") ? NUMBER : UNKNOWN);
460               case 'l': return (s.equals("null") ? NULL : UNKNOWN);
461            }
462            //return NUMBER;
463         }
464      }
465      return UNKNOWN;
466   }
467
468   private <T> BeanMap<T> parseIntoBean(XmlReader r, BeanMap<T> m) throws IOException, ParseException, ExecutableException, XMLStreamException {
469      BeanMeta<?> bMeta = m.getMeta();
470      XmlBeanMeta xmlMeta = bMeta.getExtendedMeta(XmlBeanMeta.class);
471
472      for (int i = 0; i < r.getAttributeCount(); i++) {
473         String key = getAttributeName(r, i);
474         String val = r.getAttributeValue(i);
475         String ns = r.getAttributeNamespace(i);
476         BeanPropertyMeta bpm = xmlMeta.getPropertyMeta(key);
477         if (bpm == null) {
478            if (xmlMeta.getAttrsProperty() != null) {
479               xmlMeta.getAttrsProperty().add(m, key, key, val);
480            } else if (ns == null) {
481               onUnknownProperty(key, m);
482            }
483         } else {
484            bpm.set(m, key, val);
485         }
486      }
487
488      BeanPropertyMeta cp = xmlMeta.getContentProperty();
489      XmlFormat cpf = xmlMeta.getContentFormat();
490      boolean trim = cp == null || ! cpf.isOneOf(MIXED_PWS, TEXT_PWS);
491      ClassMeta<?> cpcm = (cp == null ? object() : cp.getClassMeta());
492      StringBuilder sb = null;
493      BeanRegistry breg = cp == null ? null : cp.getBeanRegistry();
494      LinkedList<Object> l = null;
495
496      int depth = 0;
497      do {
498         int event = r.next();
499         String currAttr;
500         // We only care about text in MIXED mode.
501         // Ignore if in ELEMENTS mode.
502         if (event == CHARACTERS) {
503            if (cp != null && cpf.isOneOf(MIXED, MIXED_PWS)) {
504               if (cpcm.isCollectionOrArray()) {
505                  if (l == null)
506                     l = new LinkedList<>();
507                  l.add(getText(r, false));
508               } else {
509                  cp.set(m, null, getText(r, trim));
510               }
511            } else if (cpf != ELEMENTS) {
512               String s = getText(r, trim);
513               if (s != null) {
514                  if (sb == null)
515                     sb = getStringBuilder();
516                  sb.append(s);
517               }
518            } else {
519               // Do nothing...we're in ELEMENTS mode.
520            }
521         } else if (event == START_ELEMENT) {
522            if (cp != null && cpf.isOneOf(TEXT, TEXT_PWS)) {
523               String s = parseText(r);
524               if (s != null) {
525                  if (sb == null)
526                     sb = getStringBuilder();
527                  sb.append(s);
528               }
529               depth--;
530            } else if (cpf == XMLTEXT) {
531               if (sb == null)
532                  sb = getStringBuilder();
533               sb.append(getElementAsString(r));
534               depth++;
535            } else if (cp != null && cpf.isOneOf(MIXED, MIXED_PWS)) {
536               if (isWhitespaceElement(r) && (breg == null || ! breg.hasName(r.getLocalName()))) {
537                  if (cpcm.isCollectionOrArray()) {
538                     if (l == null)
539                        l = new LinkedList<>();
540                     l.add(parseWhitespaceElement(r));
541                  } else {
542                     cp.set(m, null, parseWhitespaceElement(r));
543                  }
544               } else {
545                  if (cpcm.isCollectionOrArray()) {
546                     if (l == null)
547                        l = new LinkedList<>();
548                     l.add(parseAnything(cpcm.getElementType(), cp.getName(), r, m.getBean(false), false, cp));
549                  } else {
550                     cp.set(m, null, parseAnything(cpcm, cp.getName(), r, m.getBean(false), false, cp));
551                  }
552               }
553            } else if (cp != null && cpf == ELEMENTS) {
554               cp.add(m, null, parseAnything(cpcm.getElementType(), cp.getName(), r, m.getBean(false), false, cp));
555            } else {
556               currAttr = getElementName(r);
557               BeanPropertyMeta pMeta = xmlMeta.getPropertyMeta(currAttr);
558               if (pMeta == null) {
559                  onUnknownProperty(currAttr, m);
560                  skipCurrentTag(r);
561               } else {
562                  setCurrentProperty(pMeta);
563                  XmlFormat xf = pMeta.getExtendedMeta(XmlBeanPropertyMeta.class).getXmlFormat();
564                  if (xf == COLLAPSED) {
565                     ClassMeta<?> et = pMeta.getClassMeta().getElementType();
566                     Object value = parseAnything(et, currAttr, r, m.getBean(false), false, pMeta);
567                     setName(et, value, currAttr);
568                     pMeta.add(m, currAttr, value);
569                  } else if (xf == ATTR)  {
570                     pMeta.set(m, currAttr, getAttributeValue(r, 0));
571                     r.nextTag();
572                  } else {
573                     ClassMeta<?> cm = pMeta.getClassMeta();
574                     Object value = parseAnything(cm, currAttr, r, m.getBean(false), false, pMeta);
575                     setName(cm, value, currAttr);
576                     pMeta.set(m, currAttr, value);
577                  }
578                  setCurrentProperty(null);
579               }
580            }
581         } else if (event == END_ELEMENT) {
582            if (depth > 0) {
583               if (cpf == XMLTEXT) {
584                  if (sb == null)
585                     sb = getStringBuilder();
586                  sb.append(getElementAsString(r));
587               }
588               else
589                  throw new ParseException("End element found where one was not expected.  {0}", XmlUtils.toReadableEvent(r));
590            }
591            depth--;
592         } else if (event == COMMENT) {
593            // Ignore comments.
594         } else {
595            throw new ParseException("Unexpected event type: {0}", XmlUtils.toReadableEvent(r));
596         }
597      } while (depth >= 0);
598
599      if (sb != null && cp != null)
600         cp.set(m, null, sb.toString());
601      else if (l != null && cp != null)
602         cp.set(m, null, XmlUtils.collapseTextNodes(l));
603
604      returnStringBuilder(sb);
605      return m;
606   }
607
608   private static void skipCurrentTag(XmlReader r) throws XMLStreamException {
609      int depth = 1;
610      do {
611         int event = r.next();
612         if (event == START_ELEMENT)
613            depth++;
614         else if (event == END_ELEMENT)
615            depth--;
616      } while (depth > 0);
617   }
618
619   private Object getUnknown(XmlReader r) throws IOException, ParseException, ExecutableException, XMLStreamException {
620      if (r.getEventType() != START_ELEMENT) {
621         throw new ParseException(this, "Parser must be on START_ELEMENT to read next text.");
622      }
623      ObjectMap m = null;
624
625      // If this element has attributes, then it's always an ObjectMap.
626      if (r.getAttributeCount() > 0) {
627         m = new ObjectMap(this);
628         for (int i = 0; i < r.getAttributeCount(); i++) {
629            String key = getAttributeName(r, i);
630            String val = r.getAttributeValue(i);
631            if (! key.equals(getBeanTypePropertyName(null)))
632               m.put(key, val);
633         }
634      }
635      int eventType = r.next();
636      StringBuilder sb = getStringBuilder();
637      while (eventType != END_ELEMENT) {
638         if (eventType == CHARACTERS || eventType == CDATA || eventType == SPACE || eventType == ENTITY_REFERENCE) {
639            sb.append(r.getText());
640         } else if (eventType == PROCESSING_INSTRUCTION || eventType == COMMENT) {
641            // skipping
642         } else if (eventType == END_DOCUMENT) {
643            throw new ParseException(this, "Unexpected end of document when reading element text content");
644         } else if (eventType == START_ELEMENT) {
645            // Oops...this has an element in it.
646            // Parse it as a map.
647            if (m == null)
648               m = new ObjectMap(this);
649            int depth = 0;
650            do {
651               int event = (eventType == -1 ? r.nextTag() : eventType);
652               String currAttr;
653               if (event == START_ELEMENT) {
654                  depth++;
655                  currAttr = getElementName(r);
656                  String key = convertAttrToType(null, currAttr, string());
657                  Object value = parseAnything(object(), currAttr, r, null, false, null);
658                  if (m.containsKey(key)) {
659                     Object o = m.get(key);
660                     if (o instanceof ObjectList)
661                        ((ObjectList)o).add(value);
662                     else
663                        m.put(key, new ObjectList(o, value).setBeanSession(this));
664                  } else {
665                     m.put(key, value);
666                  }
667
668               } else if (event == END_ELEMENT) {
669                  depth--;
670                  break;
671               }
672               eventType = -1;
673            } while (depth > 0);
674            break;
675         } else {
676            throw new ParseException(this, "Unexpected event type ''{0}''", eventType);
677         }
678         eventType = r.next();
679      }
680      String s = sb.toString().trim();
681      returnStringBuilder(sb);
682      s = decodeString(s);
683      if (m != null) {
684         if (! s.isEmpty())
685            m.put("contents", s);
686         return m;
687      }
688      return s;
689   }
690
691   //-----------------------------------------------------------------------------------------------------------------
692   // Properties
693   //-----------------------------------------------------------------------------------------------------------------
694
695   /**
696    * Configuration property:  XML event allocator.
697    *
698    * @see XmlParser#XML_eventAllocator
699    * @return
700    *    The {@link XMLEventAllocator} associated with this parser, or <jk>null</jk> if there isn't one.
701    */
702   protected final XMLEventAllocator getEventAllocator() {
703      return ctx.getEventAllocator();
704   }
705
706   /**
707    * Configuration property:  Preserve root element during generalized parsing.
708    *
709    * @see XmlParser#XML_preserveRootElement
710    * @return
711    *    <jk>true</jk> if when parsing into a generic {@link ObjectMap}, the map will contain a single entry whose key
712    *    is the root element name.
713    */
714   protected final boolean isPreserveRootElement() {
715      return ctx.isPreserveRootElement();
716   }
717
718   /**
719    * Configuration property:  XML reporter.
720    *
721    * @see XmlParser#XML_reporter
722    * @return
723    *    The {@link XMLReporter} associated with this parser, or <jk>null</jk> if there isn't one.
724    */
725   protected final XMLReporter getReporter() {
726      return ctx.getReporter();
727   }
728
729   /**
730    * Configuration property:  XML resolver.
731    *
732    * @see XmlParser#XML_resolver
733    * @return
734    *    The {@link XMLResolver} associated with this parser, or <jk>null</jk> if there isn't one.
735    */
736   protected final XMLResolver getResolver() {
737      return ctx.getResolver();
738   }
739
740   /**
741    * Configuration property:  Enable validation.
742    *
743    * @see XmlParser#XML_validating
744    * @return
745    *    <jk>true</jk> if XML document will be validated.
746    */
747   protected final boolean isValidating() {
748      return ctx.isValidating();
749   }
750
751   //-----------------------------------------------------------------------------------------------------------------
752   // Other methods
753   //-----------------------------------------------------------------------------------------------------------------
754
755   @Override /* Session */
756   public ObjectMap toMap() {
757      return super.toMap()
758         .append("XmlParserSession", new DefaultFilteringObjectMap()
759         );
760   }
761}