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