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