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
291      if (sType.isOptional()) 
292         return (T)Optional.ofNullable(parseAnything(eType.getElementType(), currAttr, r, outer, isRoot, pMeta));
293
294      setCurrentClass(sType);
295
296      String wrapperAttr = (isRoot && isPreserveRootElement()) ? r.getName().getLocalPart() : null;
297      String typeAttr = r.getAttributeValue(null, getBeanTypePropertyName(eType));
298      int jsonType = getJsonType(typeAttr);
299      String elementName = getElementName(r);
300      if (jsonType == 0) {
301         if (elementName == null || elementName.equals(currAttr))
302            jsonType = UNKNOWN;
303         else {
304            typeAttr = elementName;
305            jsonType = getJsonType(elementName);
306         }
307      }
308
309      ClassMeta tcm = getClassMeta(typeAttr, pMeta, eType);
310      if (tcm == null && elementName != null && ! elementName.equals(currAttr))
311         tcm = getClassMeta(elementName, pMeta, eType);
312      if (tcm != null)
313         sType = eType = tcm;
314
315      Object o = null;
316
317      if (jsonType == NULL) {
318         r.nextTag();   // Discard end tag
319         return null;
320      }
321
322      if (sType.isObject()) {
323         if (jsonType == OBJECT) {
324            ObjectMap m = new ObjectMap(this);
325            parseIntoMap(r, m, string(), object(), pMeta);
326            if (wrapperAttr != null)
327               m = new ObjectMap(this).append(wrapperAttr, m);
328            o = cast(m, pMeta, eType);
329         } else if (jsonType == ARRAY)
330            o = parseIntoCollection(r, new ObjectList(this), null, pMeta);
331         else if (jsonType == STRING) {
332            o = getElementText(r);
333            if (sType.isChar())
334               o = parseCharacter(o);
335         }
336         else if (jsonType == NUMBER)
337            o = parseNumber(getElementText(r), null);
338         else if (jsonType == BOOLEAN)
339            o = Boolean.parseBoolean(getElementText(r));
340         else if (jsonType == UNKNOWN)
341            o = getUnknown(r);
342      } else if (sType.isBoolean()) {
343         o = Boolean.parseBoolean(getElementText(r));
344      } else if (sType.isCharSequence()) {
345         o = getElementText(r);
346      } else if (sType.isChar()) {
347         o = parseCharacter(getElementText(r));
348      } else if (sType.isMap()) {
349         Map m = (sType.canCreateNewInstance(outer) ? (Map)sType.newInstance(outer) : new ObjectMap(this));
350         o = parseIntoMap(r, m, sType.getKeyType(), sType.getValueType(), pMeta);
351         if (wrapperAttr != null)
352            o = new ObjectMap(this).append(wrapperAttr, m);
353      } else if (sType.isCollection()) {
354         Collection l = (sType.canCreateNewInstance(outer) ? (Collection)sType.newInstance(outer) : new ObjectList(this));
355         o = parseIntoCollection(r, l, sType, pMeta);
356      } else if (sType.isNumber()) {
357         o = parseNumber(getElementText(r), (Class<? extends Number>)sType.getInnerClass());
358      } else if (builder != null || sType.canCreateNewBean(outer)) {
359         if (sType.getExtendedMeta(XmlClassMeta.class).getFormat() == COLLAPSED) {
360            String fieldName = r.getLocalName();
361            BeanMap<?> m = builder != null ? toBeanMap(builder.create(this, eType)) : newBeanMap(outer, sType.getInnerClass());
362            BeanPropertyMeta bpm = m.getMeta().getExtendedMeta(XmlBeanMeta.class).getPropertyMeta(fieldName);
363            ClassMeta<?> cm = m.getMeta().getClassMeta();
364            Object value = parseAnything(cm, currAttr, r, m.getBean(false), false, null);
365            setName(cm, value, currAttr);
366            bpm.set(m, currAttr, value);
367            o = builder != null ? builder.build(this, m.getBean(), eType) : m.getBean();
368         } else {
369            BeanMap m = builder != null ? toBeanMap(builder.create(this, eType)) : newBeanMap(outer, sType.getInnerClass());
370            m = parseIntoBean(r, m);
371            o = builder != null ? builder.build(this, m.getBean(), eType) : m.getBean();
372         }
373      } else if (sType.isArray() || sType.isArgs()) {
374         ArrayList l = (ArrayList)parseIntoCollection(r, new ArrayList(), sType, pMeta);
375         o = toArray(sType, l);
376      } else if (sType.canCreateNewInstanceFromString(outer)) {
377         o = sType.newInstanceFromString(outer, getElementText(r));
378      } else {
379         throw new ParseException(this,
380            "Class ''{0}'' could not be instantiated.  Reason: ''{1}'', property: ''{2}''",
381            sType.getInnerClass().getName(), sType.getNotABeanReason(), pMeta == null ? null : pMeta.getName());
382      }
383
384      if (swap != null && o != null)
385         o = unswap(swap, o, eType);
386
387      if (outer != null)
388         setParent(eType, o, outer);
389
390      return (T)o;
391   }
392
393   private <K,V> Map<K,V> parseIntoMap(XmlReader r, Map<K,V> m, ClassMeta<K> keyType,
394         ClassMeta<V> valueType, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException, XMLStreamException {
395      int depth = 0;
396      for (int i = 0; i < r.getAttributeCount(); i++) {
397         String a = r.getAttributeLocalName(i);
398         // TODO - Need better handling of namespaces here.
399         if (! (a.equals(getBeanTypePropertyName(null)))) {
400            K key = trim(convertAttrToType(m, a, keyType));
401            V value = trim(convertAttrToType(m, r.getAttributeValue(i), valueType));
402            setName(valueType, value, key);
403            m.put(key, value);
404         }
405      }
406      do {
407         int event = r.nextTag();
408         String currAttr;
409         if (event == START_ELEMENT) {
410            depth++;
411            currAttr = getElementName(r);
412            K key = convertAttrToType(m, currAttr, keyType);
413            V value = parseAnything(valueType, currAttr, r, m, false, pMeta);
414            setName(valueType, value, currAttr);
415            if (valueType.isObject() && m.containsKey(key)) {
416               Object o = m.get(key);
417               if (o instanceof List)
418                  ((List)o).add(value);
419               else
420                  m.put(key, (V)new ObjectList(o, value).setBeanSession(this));
421            } else {
422               m.put(key, value);
423            }
424         } else if (event == END_ELEMENT) {
425            depth--;
426            return m;
427         }
428      } while (depth > 0);
429      return m;
430   }
431
432   private <E> Collection<E> parseIntoCollection(XmlReader r, Collection<E> l,
433         ClassMeta<?> type, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException, XMLStreamException {
434      int depth = 0;
435      int argIndex = 0;
436      do {
437         int event = r.nextTag();
438         if (event == START_ELEMENT) {
439            depth++;
440            ClassMeta<?> elementType = type == null ? object() : type.isArgs() ? type.getArg(argIndex++) : type.getElementType();
441            E value = (E)parseAnything(elementType, null, r, l, false, pMeta);
442            l.add(value);
443         } else if (event == END_ELEMENT) {
444            depth--;
445            return l;
446         }
447      } while (depth > 0);
448      return l;
449   }
450
451   private static int getJsonType(String s) {
452      if (s == null)
453         return UNKNOWN;
454      char c = s.charAt(0);
455      switch(c) {
456         case 'o': return (s.equals("object") ? OBJECT : UNKNOWN);
457         case 'a': return (s.equals("array") ? ARRAY : UNKNOWN);
458         case 's': return (s.equals("string") ? STRING : UNKNOWN);
459         case 'b': return (s.equals("boolean") ? BOOLEAN : UNKNOWN);
460         case 'n': {
461            c = s.charAt(2);
462            switch(c) {
463               case 'm': return (s.equals("number") ? NUMBER : UNKNOWN);
464               case 'l': return (s.equals("null") ? NULL : UNKNOWN);
465            }
466            //return NUMBER;
467         }
468      }
469      return UNKNOWN;
470   }
471
472   private <T> BeanMap<T> parseIntoBean(XmlReader r, BeanMap<T> m) throws IOException, ParseException, ExecutableException, XMLStreamException {
473      BeanMeta<?> bMeta = m.getMeta();
474      XmlBeanMeta xmlMeta = bMeta.getExtendedMeta(XmlBeanMeta.class);
475
476      for (int i = 0; i < r.getAttributeCount(); i++) {
477         String key = getAttributeName(r, i);
478         String val = r.getAttributeValue(i);
479         String ns = r.getAttributeNamespace(i);
480         BeanPropertyMeta bpm = xmlMeta.getPropertyMeta(key);
481         if (bpm == null) {
482            if (xmlMeta.getAttrsProperty() != null) {
483               xmlMeta.getAttrsProperty().add(m, key, key, val);
484            } else if (ns == null) {
485               onUnknownProperty(key, m);
486            }
487         } else {
488            bpm.set(m, key, val);
489         }
490      }
491
492      BeanPropertyMeta cp = xmlMeta.getContentProperty();
493      XmlFormat cpf = xmlMeta.getContentFormat();
494      boolean trim = cp == null || ! cpf.isOneOf(MIXED_PWS, TEXT_PWS);
495      ClassMeta<?> cpcm = (cp == null ? object() : cp.getClassMeta());
496      StringBuilder sb = null;
497      BeanRegistry breg = cp == null ? null : cp.getBeanRegistry();
498      LinkedList<Object> l = null;
499
500      int depth = 0;
501      do {
502         int event = r.next();
503         String currAttr;
504         // We only care about text in MIXED mode.
505         // Ignore if in ELEMENTS mode.
506         if (event == CHARACTERS) {
507            if (cp != null && cpf.isOneOf(MIXED, MIXED_PWS)) {
508               if (cpcm.isCollectionOrArray()) {
509                  if (l == null)
510                     l = new LinkedList<>();
511                  l.add(getText(r, false));
512               } else {
513                  cp.set(m, null, getText(r, trim));
514               }
515            } else if (cpf != ELEMENTS) {
516               String s = getText(r, trim);
517               if (s != null) {
518                  if (sb == null)
519                     sb = getStringBuilder();
520                  sb.append(s);
521               }
522            } else {
523               // Do nothing...we're in ELEMENTS mode.
524            }
525         } else if (event == START_ELEMENT) {
526            if (cp != null && cpf.isOneOf(TEXT, TEXT_PWS)) {
527               String s = parseText(r);
528               if (s != null) {
529                  if (sb == null)
530                     sb = getStringBuilder();
531                  sb.append(s);
532               }
533               depth--;
534            } else if (cpf == XMLTEXT) {
535               if (sb == null)
536                  sb = getStringBuilder();
537               sb.append(getElementAsString(r));
538               depth++;
539            } else if (cp != null && cpf.isOneOf(MIXED, MIXED_PWS)) {
540               if (isWhitespaceElement(r) && (breg == null || ! breg.hasName(r.getLocalName()))) {
541                  if (cpcm.isCollectionOrArray()) {
542                     if (l == null)
543                        l = new LinkedList<>();
544                     l.add(parseWhitespaceElement(r));
545                  } else {
546                     cp.set(m, null, parseWhitespaceElement(r));
547                  }
548               } else {
549                  if (cpcm.isCollectionOrArray()) {
550                     if (l == null)
551                        l = new LinkedList<>();
552                     l.add(parseAnything(cpcm.getElementType(), cp.getName(), r, m.getBean(false), false, cp));
553                  } else {
554                     cp.set(m, null, parseAnything(cpcm, cp.getName(), r, m.getBean(false), false, cp));
555                  }
556               }
557            } else if (cp != null && cpf == ELEMENTS) {
558               cp.add(m, null, parseAnything(cpcm.getElementType(), cp.getName(), r, m.getBean(false), false, cp));
559            } else {
560               currAttr = getElementName(r);
561               BeanPropertyMeta pMeta = xmlMeta.getPropertyMeta(currAttr);
562               if (pMeta == null) {
563                  onUnknownProperty(currAttr, m);
564                  skipCurrentTag(r);
565               } else {
566                  setCurrentProperty(pMeta);
567                  XmlFormat xf = pMeta.getExtendedMeta(XmlBeanPropertyMeta.class).getXmlFormat();
568                  if (xf == COLLAPSED) {
569                     ClassMeta<?> et = pMeta.getClassMeta().getElementType();
570                     Object value = parseAnything(et, currAttr, r, m.getBean(false), false, pMeta);
571                     setName(et, value, currAttr);
572                     pMeta.add(m, currAttr, value);
573                  } else if (xf == ATTR)  {
574                     pMeta.set(m, currAttr, getAttributeValue(r, 0));
575                     r.nextTag();
576                  } else {
577                     ClassMeta<?> cm = pMeta.getClassMeta();
578                     Object value = parseAnything(cm, currAttr, r, m.getBean(false), false, pMeta);
579                     setName(cm, value, currAttr);
580                     pMeta.set(m, currAttr, value);
581                  }
582                  setCurrentProperty(null);
583               }
584            }
585         } else if (event == END_ELEMENT) {
586            if (depth > 0) {
587               if (cpf == XMLTEXT) {
588                  if (sb == null)
589                     sb = getStringBuilder();
590                  sb.append(getElementAsString(r));
591               }
592               else
593                  throw new ParseException("End element found where one was not expected.  {0}", XmlUtils.toReadableEvent(r));
594            }
595            depth--;
596         } else if (event == COMMENT) {
597            // Ignore comments.
598         } else {
599            throw new ParseException("Unexpected event type: {0}", XmlUtils.toReadableEvent(r));
600         }
601      } while (depth >= 0);
602
603      if (sb != null && cp != null)
604         cp.set(m, null, sb.toString());
605      else if (l != null && cp != null)
606         cp.set(m, null, XmlUtils.collapseTextNodes(l));
607
608      returnStringBuilder(sb);
609      return m;
610   }
611
612   private static void skipCurrentTag(XmlReader r) throws XMLStreamException {
613      int depth = 1;
614      do {
615         int event = r.next();
616         if (event == START_ELEMENT)
617            depth++;
618         else if (event == END_ELEMENT)
619            depth--;
620      } while (depth > 0);
621   }
622
623   private Object getUnknown(XmlReader r) throws IOException, ParseException, ExecutableException, XMLStreamException {
624      if (r.getEventType() != START_ELEMENT) {
625         throw new ParseException(this, "Parser must be on START_ELEMENT to read next text.");
626      }
627      ObjectMap m = null;
628
629      // If this element has attributes, then it's always an ObjectMap.
630      if (r.getAttributeCount() > 0) {
631         m = new ObjectMap(this);
632         for (int i = 0; i < r.getAttributeCount(); i++) {
633            String key = getAttributeName(r, i);
634            String val = r.getAttributeValue(i);
635            if (! key.equals(getBeanTypePropertyName(null)))
636               m.put(key, val);
637         }
638      }
639      int eventType = r.next();
640      StringBuilder sb = getStringBuilder();
641      while (eventType != END_ELEMENT) {
642         if (eventType == CHARACTERS || eventType == CDATA || eventType == SPACE || eventType == ENTITY_REFERENCE) {
643            sb.append(r.getText());
644         } else if (eventType == PROCESSING_INSTRUCTION || eventType == COMMENT) {
645            // skipping
646         } else if (eventType == END_DOCUMENT) {
647            throw new ParseException(this, "Unexpected end of document when reading element text content");
648         } else if (eventType == START_ELEMENT) {
649            // Oops...this has an element in it.
650            // Parse it as a map.
651            if (m == null)
652               m = new ObjectMap(this);
653            int depth = 0;
654            do {
655               int event = (eventType == -1 ? r.nextTag() : eventType);
656               String currAttr;
657               if (event == START_ELEMENT) {
658                  depth++;
659                  currAttr = getElementName(r);
660                  String key = convertAttrToType(null, currAttr, string());
661                  Object value = parseAnything(object(), currAttr, r, null, false, null);
662                  if (m.containsKey(key)) {
663                     Object o = m.get(key);
664                     if (o instanceof ObjectList)
665                        ((ObjectList)o).add(value);
666                     else
667                        m.put(key, new ObjectList(o, value).setBeanSession(this));
668                  } else {
669                     m.put(key, value);
670                  }
671
672               } else if (event == END_ELEMENT) {
673                  depth--;
674                  break;
675               }
676               eventType = -1;
677            } while (depth > 0);
678            break;
679         } else {
680            throw new ParseException(this, "Unexpected event type ''{0}''", eventType);
681         }
682         eventType = r.next();
683      }
684      String s = sb.toString().trim();
685      returnStringBuilder(sb);
686      s = decodeString(s);
687      if (m != null) {
688         if (! s.isEmpty())
689            m.put("contents", s);
690         return m;
691      }
692      return s;
693   }
694
695   //-----------------------------------------------------------------------------------------------------------------
696   // Properties
697   //-----------------------------------------------------------------------------------------------------------------
698
699   /**
700    * Configuration property:  XML event allocator.
701    *
702    * @see XmlParser#XML_eventAllocator
703    * @return
704    *    The {@link XMLEventAllocator} associated with this parser, or <jk>null</jk> if there isn't one.
705    */
706   protected final XMLEventAllocator getEventAllocator() {
707      return ctx.getEventAllocator();
708   }
709
710   /**
711    * Configuration property:  Preserve root element during generalized parsing.
712    *
713    * @see XmlParser#XML_preserveRootElement
714    * @return
715    *    <jk>true</jk> if when parsing into a generic {@link ObjectMap}, the map will contain a single entry whose key
716    *    is the root element name.
717    */
718   protected final boolean isPreserveRootElement() {
719      return ctx.isPreserveRootElement();
720   }
721
722   /**
723    * Configuration property:  XML reporter.
724    *
725    * @see XmlParser#XML_reporter
726    * @return
727    *    The {@link XMLReporter} associated with this parser, or <jk>null</jk> if there isn't one.
728    */
729   protected final XMLReporter getReporter() {
730      return ctx.getReporter();
731   }
732
733   /**
734    * Configuration property:  XML resolver.
735    *
736    * @see XmlParser#XML_resolver
737    * @return
738    *    The {@link XMLResolver} associated with this parser, or <jk>null</jk> if there isn't one.
739    */
740   protected final XMLResolver getResolver() {
741      return ctx.getResolver();
742   }
743
744   /**
745    * Configuration property:  Enable validation.
746    *
747    * @see XmlParser#XML_validating
748    * @return
749    *    <jk>true</jk> if XML document will be validated.
750    */
751   protected final boolean isValidating() {
752      return ctx.isValidating();
753   }
754
755   //-----------------------------------------------------------------------------------------------------------------
756   // Other methods
757   //-----------------------------------------------------------------------------------------------------------------
758
759   @Override /* Session */
760   public ObjectMap toMap() {
761      return super.toMap()
762         .append("XmlParserSession", new DefaultFilteringObjectMap()
763         );
764   }
765}