001/**
002 *
003 * Copyright 2003-2007 Jive Software, 2019-2023 Florian Schmaus.
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.jivesoftware.smack.util;
018
019import java.io.IOException;
020import java.io.InputStream;
021import java.io.InputStreamReader;
022import java.io.Reader;
023import java.io.StringReader;
024import java.nio.charset.StandardCharsets;
025import java.util.ArrayList;
026import java.util.Collection;
027import java.util.HashMap;
028import java.util.LinkedList;
029import java.util.List;
030import java.util.Map;
031import java.util.logging.Level;
032import java.util.logging.Logger;
033
034import org.jivesoftware.smack.compress.packet.Compress;
035import org.jivesoftware.smack.packet.EmptyResultIQ;
036import org.jivesoftware.smack.packet.ErrorIQ;
037import org.jivesoftware.smack.packet.ExtensionElement;
038import org.jivesoftware.smack.packet.IQ;
039import org.jivesoftware.smack.packet.IqData;
040import org.jivesoftware.smack.packet.Message;
041import org.jivesoftware.smack.packet.MessageBuilder;
042import org.jivesoftware.smack.packet.Presence;
043import org.jivesoftware.smack.packet.PresenceBuilder;
044import org.jivesoftware.smack.packet.Session;
045import org.jivesoftware.smack.packet.Stanza;
046import org.jivesoftware.smack.packet.StanzaBuilder;
047import org.jivesoftware.smack.packet.StanzaError;
048import org.jivesoftware.smack.packet.StartTls;
049import org.jivesoftware.smack.packet.StreamError;
050import org.jivesoftware.smack.packet.UnparsedIQ;
051import org.jivesoftware.smack.packet.XmlElement;
052import org.jivesoftware.smack.packet.XmlEnvironment;
053import org.jivesoftware.smack.parsing.SmackParsingException;
054import org.jivesoftware.smack.parsing.StandardExtensionElementProvider;
055import org.jivesoftware.smack.provider.ExtensionElementProvider;
056import org.jivesoftware.smack.provider.IqProvider;
057import org.jivesoftware.smack.provider.ProviderManager;
058import org.jivesoftware.smack.xml.SmackXmlParser;
059import org.jivesoftware.smack.xml.XmlPullParser;
060import org.jivesoftware.smack.xml.XmlPullParserException;
061
062import org.jxmpp.jid.Jid;
063import org.jxmpp.stringprep.XmppStringprepException;
064
065/**
066 * Utility class that helps to parse packets. Any parsing packets method that must be shared
067 * between many clients must be placed in this utility class.
068 *
069 * @author Gaston Dombiak
070 */
071public class PacketParserUtils {
072    private static final Logger LOGGER = Logger.getLogger(PacketParserUtils.class.getName());
073
074    // TODO: Rename argument name from 'stanza' to 'element'.
075    public static XmlPullParser getParserFor(String stanza) throws XmlPullParserException, IOException {
076        return getParserFor(new StringReader(stanza));
077    }
078
079    public static XmlPullParser getParserFor(InputStream inputStream) throws XmlPullParserException, IOException {
080        InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
081        return getParserFor(inputStreamReader);
082    }
083
084    public static XmlPullParser getParserFor(Reader reader) throws XmlPullParserException, IOException {
085        XmlPullParser parser = SmackXmlParser.newXmlParser(reader);
086        ParserUtils.forwardToStartElement(parser);
087        return parser;
088    }
089
090    @SuppressWarnings("unchecked")
091    public static <S extends Stanza> S parseStanza(String stanza) throws XmlPullParserException, SmackParsingException, IOException {
092        return (S) parseStanza(getParserFor(stanza), XmlEnvironment.EMPTY);
093    }
094
095    /**
096     * Tries to parse and return either a Message, IQ or Presence stanza.
097     *
098     * connection is optional and is used to return feature-not-implemented errors for unknown IQ stanzas.
099     *
100     * @param parser TODO javadoc me please
101     * @param outerXmlEnvironment the outer XML environment (optional).
102     * @return a stanza which is either a Message, IQ or Presence.
103     * @throws XmlPullParserException if an error in the XML parser occurred.
104     * @throws SmackParsingException if the Smack parser (provider) encountered invalid input.
105     * @throws IOException if an I/O error occurred.
106     */
107    public static Stanza parseStanza(XmlPullParser parser, XmlEnvironment outerXmlEnvironment) throws XmlPullParserException, SmackParsingException, IOException {
108        ParserUtils.assertAtStartTag(parser);
109        final String name = parser.getName();
110        switch (name) {
111        case Message.ELEMENT:
112            return parseMessage(parser, outerXmlEnvironment);
113        case IQ.IQ_ELEMENT:
114            return parseIQ(parser, outerXmlEnvironment);
115        case Presence.ELEMENT:
116            return parsePresence(parser, outerXmlEnvironment);
117        default:
118            throw new IllegalArgumentException("Can only parse message, iq or presence, not " + name);
119        }
120    }
121
122    private interface StanzaBuilderSupplier<SB extends StanzaBuilder<?>> {
123        SB get(String stanzaId);
124    }
125
126    private static <SB extends StanzaBuilder<?>> SB parseCommonStanzaAttributes(StanzaBuilderSupplier<SB> stanzaBuilderSupplier, XmlPullParser parser, XmlEnvironment xmlEnvironment) throws XmppStringprepException {
127        String id = parser.getAttributeValue("id");
128
129        SB stanzaBuilder = stanzaBuilderSupplier.get(id);
130
131        Jid to = ParserUtils.getJidAttribute(parser, "to");
132        stanzaBuilder.to(to);
133
134        Jid from = ParserUtils.getJidAttribute(parser, "from");
135        stanzaBuilder.from(from);
136
137        String language = ParserUtils.getXmlLang(parser, xmlEnvironment);
138        stanzaBuilder.setLanguage(language);
139
140        return stanzaBuilder;
141    }
142
143    public static Message parseMessage(XmlPullParser parser) throws XmlPullParserException, IOException, SmackParsingException {
144        return parseMessage(parser, XmlEnvironment.EMPTY);
145    }
146
147    /**
148     * Parses a message packet.
149     *
150     * @param parser the XML parser, positioned at the start of a message packet.
151     * @param outerXmlEnvironment the outer XML environment (optional).
152     * @return a Message packet.
153     * @throws XmlPullParserException if an error in the XML parser occurred.
154     * @throws IOException if an I/O error occurred.
155     * @throws SmackParsingException if the Smack parser (provider) encountered invalid input.
156     */
157    public static Message parseMessage(XmlPullParser parser, XmlEnvironment outerXmlEnvironment) throws XmlPullParserException, IOException, SmackParsingException {
158        ParserUtils.assertAtStartTag(parser);
159        assert parser.getName().equals(Message.ELEMENT);
160
161        XmlEnvironment messageXmlEnvironment = XmlEnvironment.from(parser, outerXmlEnvironment);
162        final int initialDepth = parser.getDepth();
163
164        MessageBuilder message = parseCommonStanzaAttributes(id -> {
165            return StanzaBuilder.buildMessage(id);
166        }, parser, outerXmlEnvironment);
167
168        String typeString = parser.getAttributeValue("", "type");
169        if (typeString != null) {
170            message.ofType(Message.Type.fromString(typeString));
171        }
172
173        // Parse sub-elements. We include extra logic to make sure the values
174        // are only read once. This is because it's possible for the names to appear
175        // in arbitrary sub-elements.
176        outerloop: while (true) {
177            XmlPullParser.Event eventType = parser.next();
178            switch (eventType) {
179            case START_ELEMENT:
180                String elementName = parser.getName();
181                String namespace = parser.getNamespace();
182                switch (elementName) {
183                case "error":
184                    message.setError(parseError(parser, messageXmlEnvironment));
185                    break;
186                 default:
187                     XmlElement extensionElement = parseExtensionElement(elementName, namespace, parser, messageXmlEnvironment);
188                    message.addExtension(extensionElement);
189                    break;
190                }
191                break;
192            case END_ELEMENT:
193                if (parser.getDepth() == initialDepth) {
194                    break outerloop;
195                }
196                break;
197            default: // fall out
198            }
199        }
200
201        // TODO check for duplicate body elements. This means we need to check for duplicate xml:lang pairs and for
202        // situations where we have a body element with an explicit xml lang set and once where the value is inherited
203        // and both values are equal.
204
205        return message.build();
206    }
207
208    /**
209     * Returns the textual content of an element as String. After this method returns the parser
210     * position will be END_ELEMENT, following the established pull parser calling convention.
211     * <p>
212     * The parser must be positioned on a START_ELEMENT of an element which MUST NOT contain Mixed
213     * Content (as defined in XML 3.2.2), or else an XmlPullParserException will be thrown.
214     * </p>
215     * This method is used for the parts where the XMPP specification requires elements that contain
216     * only text or are the empty element.
217     *
218     * @param parser TODO javadoc me please
219     * @return the textual content of the element as String
220     * @throws XmlPullParserException if an error in the XML parser occurred.
221     * @throws IOException if an I/O error occurred.
222     */
223    public static String parseElementText(XmlPullParser parser) throws XmlPullParserException, IOException {
224        assert parser.getEventType() == XmlPullParser.Event.START_ELEMENT;
225        String res;
226        // Advance to the text of the Element
227        XmlPullParser.Event event = parser.next();
228        if (event != XmlPullParser.Event.TEXT_CHARACTERS) {
229            if (event == XmlPullParser.Event.END_ELEMENT) {
230                // Assume this is the end tag of the start tag at the
231                // beginning of this method. Typical examples where this
232                // happens are body elements containing the empty string,
233                // ie. <body></body>, which appears to be valid XMPP, or a
234                // least it's not explicitly forbidden by RFC 6121 5.2.3
235                return "";
236            } else {
237                throw new XmlPullParserException(
238                                "Non-empty element tag not followed by text, while Mixed Content (XML 3.2.2) is disallowed");
239            }
240        }
241        res = parser.getText();
242        event = parser.next();
243        if (event != XmlPullParser.Event.END_ELEMENT) {
244            throw new XmlPullParserException(
245                            "Non-empty element tag contains child-elements, while Mixed Content (XML 3.2.2) is disallowed");
246        }
247        return res;
248    }
249
250    /**
251     * Returns the current element as string.
252     * <p>
253     * The parser must be positioned on START_ELEMENT.
254     * </p>
255     * Note that only the outermost namespace attributes ("xmlns") will be returned, not nested ones.
256     *
257     * @param parser the XML pull parser
258     * @return the element as string
259     * @throws XmlPullParserException if an error in the XML parser occurred.
260     * @throws IOException if an I/O error occurred.
261     */
262    public static CharSequence parseElement(XmlPullParser parser) throws XmlPullParserException, IOException {
263        return parseElement(parser, false);
264    }
265
266    public static CharSequence parseElement(XmlPullParser parser,
267                    boolean fullNamespaces) throws XmlPullParserException,
268                    IOException {
269        assert parser.getEventType() == XmlPullParser.Event.START_ELEMENT;
270        return parseContentDepth(parser, parser.getDepth(), fullNamespaces);
271    }
272
273    public static CharSequence parseContentDepth(XmlPullParser parser, int depth)
274                    throws XmlPullParserException, IOException {
275        return parseContentDepth(parser, depth, false);
276    }
277
278    /**
279     * Returns the content from the current position of the parser up to the closing tag of the
280     * given depth. Note that only the outermost namespace attributes ("xmlns") will be returned,
281     * not nested ones, if <code>fullNamespaces</code> is false. If it is true, then namespaces of
282     * parent elements will be added to child elements that don't define a different namespace.
283     * <p>
284     * This method is able to parse the content with MX- and KXmlParser. KXmlParser does not support
285     * xml-roundtrip. i.e. return a String on getText() on START_ELEMENT and END_ELEMENT. We check for the
286     * XML_ROUNDTRIP feature. If it's not found we are required to work around this limitation, which
287     * results in only partial support for XML namespaces ("xmlns"): Only the outermost namespace of
288     * elements will be included in the resulting String, if <code>fullNamespaces</code> is set to false.
289     * </p>
290     * <p>
291     * In particular Android's XmlPullParser does not support XML_ROUNDTRIP.
292     * </p>
293     *
294     * @param parser TODO javadoc me please
295     * @param depth TODO javadoc me please
296     * @param fullNamespaces TODO javadoc me please
297     * @return the content of the current depth
298     * @throws XmlPullParserException if an error in the XML parser occurred.
299     * @throws IOException if an I/O error occurred.
300     */
301    public static CharSequence parseContentDepth(XmlPullParser parser, int depth, boolean fullNamespaces) throws XmlPullParserException, IOException {
302        if (parser.supportsRoundtrip()) {
303            return parseContentDepthWithRoundtrip(parser, depth);
304        } else {
305            return parseContentDepthWithoutRoundtrip(parser, depth, fullNamespaces);
306        }
307    }
308
309    private static CharSequence parseContentDepthWithoutRoundtrip(XmlPullParser parser, int depth,
310                    boolean fullNamespaces) throws XmlPullParserException, IOException {
311        XmlStringBuilder xml = new XmlStringBuilder();
312        XmlPullParser.Event event = parser.getEventType();
313        // XmlPullParser reports namespaces in nested elements even if *only* the outer ones defines
314        // it. This 'flag' ensures that when a namespace is set for an element, it won't be set again
315        // in a nested element. It's an ugly workaround that has the potential to break things.
316        String namespaceElement = null;
317        boolean startElementJustSeen = false;
318        outerloop: while (true) {
319            switch (event) {
320            case START_ELEMENT:
321                if (startElementJustSeen) {
322                    xml.rightAngleBracket();
323                }
324                else {
325                    startElementJustSeen = true;
326                }
327                xml.halfOpenElement(parser.getName());
328                if (namespaceElement == null || fullNamespaces) {
329                    String namespace = parser.getNamespace();
330                    if (StringUtils.isNotEmpty(namespace)) {
331                        xml.attribute("xmlns", namespace);
332                        namespaceElement = parser.getName();
333                    }
334                }
335                for (int i = 0; i < parser.getAttributeCount(); i++) {
336                    xml.attribute(parser.getAttributeName(i), parser.getAttributeValue(i));
337                }
338                break;
339            case END_ELEMENT:
340                if (startElementJustSeen) {
341                    xml.closeEmptyElement();
342                    startElementJustSeen = false;
343                }
344                else {
345                    xml.closeElement(parser.getName());
346                }
347                if (namespaceElement != null && namespaceElement.equals(parser.getName())) {
348                    // We are on the closing tag, which defined the namespace as starting tag, reset the 'flag'
349                    namespaceElement = null;
350                }
351                if (parser.getDepth() <= depth) {
352                    // Abort parsing, we are done
353                    break outerloop;
354                }
355                break;
356            case TEXT_CHARACTERS:
357                if (startElementJustSeen) {
358                    startElementJustSeen = false;
359                    xml.rightAngleBracket();
360                }
361                xml.escape(parser.getText());
362                break;
363            default:
364                // Catch all for incomplete switch (MissingCasesInEnumSwitch) statement.
365                break;
366            }
367            event = parser.next();
368        }
369        return xml;
370    }
371
372    private static XmlStringBuilder parseContentDepthWithRoundtrip(XmlPullParser parser, int depth)
373                    throws XmlPullParserException, IOException {
374        XmlStringBuilder sb = new XmlStringBuilder();
375        XmlPullParser.Event event = parser.getEventType();
376        boolean startElementJustSeen = false;
377        outerloop: while (true) {
378            switch (event) {
379            case START_ELEMENT:
380                startElementJustSeen = true;
381                String openElementTag = parser.getText();
382                sb.append(openElementTag);
383                break;
384            case END_ELEMENT:
385                boolean isEmptyElement = false;
386                if (startElementJustSeen) {
387                    isEmptyElement = true;
388                    startElementJustSeen = false;
389                }
390                if (!isEmptyElement) {
391                    String text = parser.getText();
392                    sb.append(text);
393                }
394                if (parser.getDepth() <= depth) {
395                    break outerloop;
396                }
397                break;
398            default:
399                startElementJustSeen = false;
400                CharSequence text = parser.getText();
401                if (event == XmlPullParser.Event.TEXT_CHARACTERS) {
402                    text = StringUtils.escapeForXml(text);
403                }
404                sb.append(text);
405                break;
406            }
407            event = parser.next();
408        }
409        return sb;
410    }
411
412    public static Presence parsePresence(XmlPullParser parser) throws XmlPullParserException, IOException, SmackParsingException {
413        return parsePresence(parser, XmlEnvironment.EMPTY);
414    }
415
416    /**
417     * Parses a presence packet.
418     *
419     * @param parser the XML parser, positioned at the start of a presence packet.
420     * @param outerXmlEnvironment the outer XML environment (optional).
421     * @return a Presence packet.
422     * @throws IOException if an I/O error occurred.
423     * @throws XmlPullParserException if an error in the XML parser occurred.
424     * @throws SmackParsingException if the Smack parser (provider) encountered invalid input.
425     */
426    public static Presence parsePresence(XmlPullParser parser, XmlEnvironment outerXmlEnvironment) throws XmlPullParserException, IOException, SmackParsingException {
427        ParserUtils.assertAtStartTag(parser);
428        final int initialDepth = parser.getDepth();
429        XmlEnvironment presenceXmlEnvironment = XmlEnvironment.from(parser, outerXmlEnvironment);
430
431        PresenceBuilder presence = parseCommonStanzaAttributes(
432                        stanzaId -> StanzaBuilder.buildPresence(stanzaId), parser, outerXmlEnvironment);
433
434        Presence.Type type = Presence.Type.available;
435        String typeString = parser.getAttributeValue("", "type");
436        if (typeString != null && !typeString.equals("")) {
437            type = Presence.Type.fromString(typeString);
438        }
439
440        presence.ofType(type);
441
442        // Parse sub-elements
443        outerloop: while (true) {
444            XmlPullParser.Event eventType = parser.next();
445            switch (eventType) {
446            case START_ELEMENT:
447                String elementName = parser.getName();
448                String namespace = parser.getNamespace();
449                switch (elementName) {
450                case "status":
451                    presence.setStatus(parser.nextText());
452                    break;
453                case "priority":
454                    Byte priority = ParserUtils.getByteAttributeFromNextText(parser);
455                    presence.setPriority(priority);
456                    break;
457                case "show":
458                    String modeText = parser.nextText();
459                    if (StringUtils.isNotEmpty(modeText)) {
460                        presence.setMode(Presence.Mode.fromString(modeText));
461                    } else {
462                        // Some implementations send presence stanzas with a
463                        // '<show />' element, which is a invalid XMPP presence
464                        // stanza according to RFC 6121 4.7.2.1
465                        LOGGER.warning("Empty or null mode text in presence show element form "
466                                        + presence
467                                        + "' which is invalid according to RFC6121 4.7.2.1");
468                    }
469                    break;
470                case "error":
471                    presence.setError(parseError(parser, presenceXmlEnvironment));
472                    break;
473                default:
474                // Otherwise, it must be a packet extension.
475                    // Be extra robust: Skip PacketExtensions that cause Exceptions, instead of
476                    // failing completely here. See SMACK-390 for more information.
477                    try {
478                        XmlElement extensionElement = parseExtensionElement(elementName, namespace, parser, presenceXmlEnvironment);
479                        presence.addExtension(extensionElement);
480                    } catch (Exception e) {
481                        LOGGER.log(Level.WARNING, "Failed to parse extension element in Presence stanza: " + presence, e);
482                    }
483                    break;
484                }
485                break;
486            case END_ELEMENT:
487                if (parser.getDepth() == initialDepth) {
488                    break outerloop;
489                }
490                break;
491            default:
492                // Catch all for incomplete switch (MissingCasesInEnumSwitch) statement.
493                break;
494            }
495        }
496
497        return presence.build();
498    }
499
500    public static IQ parseIQ(XmlPullParser parser) throws Exception {
501        return parseIQ(parser, null);
502    }
503
504    public static IqData parseIqData(XmlPullParser parser) throws XmppStringprepException {
505        final String id = parser.getAttributeValue("", "id");
506        IqData iqData = StanzaBuilder.buildIqData(id);
507
508        final Jid to = ParserUtils.getJidAttribute(parser, "to");
509        iqData.to(to);
510
511        final Jid from = ParserUtils.getJidAttribute(parser, "from");
512        iqData.from(from);
513
514        String typeString = parser.getAttributeValue("", "type");
515        final IQ.Type type = IQ.Type.fromString(typeString);
516        iqData.ofType(type);
517
518        return iqData;
519    }
520
521    /**
522     * Parses an IQ packet.
523     *
524     * @param parser the XML parser, positioned at the start of an IQ packet.
525     * @param outerXmlEnvironment the outer XML environment (optional).
526     * @return an IQ object.
527     * @throws XmlPullParserException if an error in the XML parser occurred.
528     * @throws XmppStringprepException if the provided string is invalid.
529     * @throws IOException if an I/O error occurred.
530     * @throws SmackParsingException if the Smack parser (provider) encountered invalid input.
531     */
532    public static IQ parseIQ(XmlPullParser parser, XmlEnvironment outerXmlEnvironment) throws XmlPullParserException, XmppStringprepException, IOException, SmackParsingException {
533        ParserUtils.assertAtStartTag(parser);
534        final int initialDepth = parser.getDepth();
535        XmlEnvironment iqXmlEnvironment = XmlEnvironment.from(parser, outerXmlEnvironment);
536        IQ iqPacket = null;
537        StanzaError error = null;
538        IqData iqData = parseIqData(parser);
539
540        outerloop: while (true) {
541            XmlPullParser.Event eventType = parser.next();
542
543            switch (eventType) {
544            case START_ELEMENT:
545                String elementName = parser.getName();
546                String namespace = parser.getNamespace();
547                switch (elementName) {
548                case "error":
549                    error = PacketParserUtils.parseError(parser, iqXmlEnvironment);
550                    break;
551                // Otherwise, see if there is a registered provider for
552                // this element name and namespace.
553                default:
554                    IqProvider<IQ> provider = ProviderManager.getIQProvider(elementName, namespace);
555                    if (provider != null) {
556                            iqPacket = provider.parse(parser, iqData, outerXmlEnvironment);
557                    }
558                    // Note that if we reach this code, it is guaranteed that the result IQ contained a child element
559                    // (RFC 6120 ยง 8.2.3 6) because otherwise we would have reached the END_ELEMENT first.
560                    else {
561                        // No Provider found for the IQ stanza, parse it to an UnparsedIQ instance
562                        // so that the content of the IQ can be examined later on
563                        iqPacket = new UnparsedIQ(elementName, namespace, parseElement(parser));
564                    }
565                    break;
566                }
567                break;
568            case END_ELEMENT:
569                if (parser.getDepth() == initialDepth) {
570                    break outerloop;
571                }
572                break;
573            default:
574                // Catch all for incomplete switch (MissingCasesInEnumSwitch) statement.
575                break;
576            }
577        }
578        // Decide what to do when an IQ packet was not understood
579        if (iqPacket == null) {
580            switch (iqData.getType()) {
581            case error:
582                // If an IQ packet wasn't created above, create an empty error IQ packet.
583                iqPacket = ErrorIQ.builder(error, iqData).build();
584                // The following return is simply to avoid setting iqData again below.
585                return iqPacket;
586            case result:
587                iqPacket = new EmptyResultIQ();
588                break;
589            default:
590                break;
591            }
592        }
593
594        // Set basic values on the iq packet.
595        iqPacket.setStanzaId(iqData.getStanzaId());
596        iqPacket.setTo(iqData.getTo());
597        iqPacket.setFrom(iqData.getFrom());
598        iqPacket.setType(iqData.getType());
599        iqPacket.setError(error);
600
601        return iqPacket;
602    }
603
604    /**
605     * Parse the available SASL mechanisms reported from the server.
606     *
607     * @param parser the XML parser, positioned at the start of the mechanisms stanza.
608     * @return a collection of Stings with the mechanisms included in the mechanisms stanza.
609     * @throws IOException if an I/O error occurred.
610     * @throws XmlPullParserException if an error in the XML parser occurred.
611     */
612    public static Collection<String> parseMechanisms(XmlPullParser parser)
613                    throws XmlPullParserException, IOException {
614        List<String> mechanisms = new ArrayList<String>();
615        boolean done = false;
616        while (!done) {
617            XmlPullParser.Event eventType = parser.next();
618
619            if (eventType == XmlPullParser.Event.START_ELEMENT) {
620                String elementName = parser.getName();
621                if (elementName.equals("mechanism")) {
622                    mechanisms.add(parser.nextText());
623                }
624            }
625            else if (eventType == XmlPullParser.Event.END_ELEMENT) {
626                if (parser.getName().equals("mechanisms")) {
627                    done = true;
628                }
629            }
630        }
631        return mechanisms;
632    }
633
634    /**
635     * Parse the Compression Feature reported from the server.
636     *
637     * @param parser the XML parser, positioned at the start of the compression stanza.
638     * @return The CompressionFeature stream element
639     * @throws IOException if an I/O error occurred.
640     * @throws XmlPullParserException if an exception occurs while parsing the stanza.
641     */
642    public static Compress.Feature parseCompressionFeature(XmlPullParser parser)
643                    throws IOException, XmlPullParserException {
644        assert parser.getEventType() == XmlPullParser.Event.START_ELEMENT;
645        String name;
646        final int initialDepth = parser.getDepth();
647        List<String> methods = new LinkedList<>();
648        outerloop: while (true) {
649            XmlPullParser.Event eventType = parser.next();
650            switch (eventType) {
651            case START_ELEMENT:
652                name = parser.getName();
653                switch (name) {
654                case "method":
655                    methods.add(parser.nextText());
656                    break;
657                }
658                break;
659            case END_ELEMENT:
660                name = parser.getName();
661                switch (name) {
662                case Compress.Feature.ELEMENT:
663                    if (parser.getDepth() == initialDepth) {
664                        break outerloop;
665                    }
666                }
667                break;
668            default:
669                // Catch all for incomplete switch (MissingCasesInEnumSwitch) statement.
670                break;
671            }
672        }
673        assert parser.getEventType() == XmlPullParser.Event.END_ELEMENT;
674        assert parser.getDepth() == initialDepth;
675        return new Compress.Feature(methods);
676    }
677
678    public static Map<String, String> parseDescriptiveTexts(XmlPullParser parser, Map<String, String> descriptiveTexts)
679                    throws XmlPullParserException, IOException {
680        if (descriptiveTexts == null) {
681            descriptiveTexts = new HashMap<>();
682        }
683        String xmllang = ParserUtils.getXmlLang(parser);
684        if (xmllang == null) {
685            // XMPPError assumes the default locale, 'en', or the empty string.
686            // Establish the invariant that there is never null as a key.
687            xmllang = "";
688        }
689
690        String text = parser.nextText();
691        String previousValue = descriptiveTexts.put(xmllang, text);
692        assert previousValue == null;
693        return descriptiveTexts;
694    }
695
696    public static StreamError parseStreamError(XmlPullParser parser) throws XmlPullParserException, IOException, SmackParsingException {
697        return parseStreamError(parser, null);
698    }
699
700    /**
701     * Parses stream error packets.
702     *
703     * @param parser the XML parser.
704     * @param outerXmlEnvironment the outer XML environment (optional).
705     * @return an stream error packet.
706     * @throws IOException if an I/O error occurred.
707     * @throws XmlPullParserException if an error in the XML parser occurred.
708     * @throws SmackParsingException if the Smack parser (provider) encountered invalid input.
709     */
710    public static StreamError parseStreamError(XmlPullParser parser, XmlEnvironment outerXmlEnvironment) throws XmlPullParserException, IOException, SmackParsingException {
711        final int initialDepth = parser.getDepth();
712        List<XmlElement> extensions = new ArrayList<>();
713        Map<String, String> descriptiveTexts = null;
714        StreamError.Condition condition = null;
715        String conditionText = null;
716        XmlEnvironment streamErrorXmlEnvironment = XmlEnvironment.from(parser, outerXmlEnvironment);
717        outerloop: while (true) {
718            XmlPullParser.Event eventType = parser.next();
719            switch (eventType) {
720            case START_ELEMENT:
721                String name = parser.getName();
722                String namespace = parser.getNamespace();
723                switch (namespace) {
724                case StreamError.NAMESPACE:
725                    switch (name) {
726                    case "text":
727                        descriptiveTexts = parseDescriptiveTexts(parser, descriptiveTexts);
728                        break;
729                    default:
730                        // If it's not a text element, that is qualified by the StreamError.NAMESPACE,
731                        // then it has to be the stream error code
732                        condition = StreamError.Condition.fromString(name);
733                        conditionText = parser.nextText();
734                        if (conditionText.isEmpty()) {
735                            conditionText = null;
736                        }
737                        break;
738                    }
739                    break;
740                default:
741                    PacketParserUtils.addExtensionElement(extensions, parser, name, namespace, streamErrorXmlEnvironment);
742                    break;
743                }
744                break;
745            case END_ELEMENT:
746                if (parser.getDepth() == initialDepth) {
747                    break outerloop;
748                }
749                break;
750            default:
751                // Catch all for incomplete switch (MissingCasesInEnumSwitch) statement.
752                break;
753            }
754        }
755        return new StreamError(condition, conditionText, descriptiveTexts, extensions);
756    }
757
758    public static StanzaError parseError(XmlPullParser parser) throws XmlPullParserException, IOException, SmackParsingException {
759        return parseError(parser, null);
760    }
761
762    /**
763     * Parses error sub-packets.
764     *
765     * @param parser the XML parser.
766     * @param outerXmlEnvironment the outer XML environment (optional).
767     * @return an error sub-packet.
768     * @throws IOException if an I/O error occurred.
769     * @throws XmlPullParserException if an error in the XML parser occurred.
770     * @throws SmackParsingException if the Smack parser (provider) encountered invalid input.
771     */
772    public static StanzaError parseError(XmlPullParser parser, XmlEnvironment outerXmlEnvironment) throws XmlPullParserException, IOException, SmackParsingException {
773        final int initialDepth = parser.getDepth();
774        Map<String, String> descriptiveTexts = null;
775        XmlEnvironment stanzaErrorXmlEnvironment = XmlEnvironment.from(parser, outerXmlEnvironment);
776        List<XmlElement> extensions = new ArrayList<>();
777        StanzaError.Builder builder = StanzaError.getBuilder();
778
779        // Parse the error header
780        builder.setType(StanzaError.Type.fromString(parser.getAttributeValue("", "type")));
781        builder.setErrorGenerator(parser.getAttributeValue("", "by"));
782
783        outerloop: while (true) {
784            XmlPullParser.Event eventType = parser.next();
785            switch (eventType) {
786            case START_ELEMENT:
787                String name = parser.getName();
788                String namespace = parser.getNamespace();
789                switch (namespace) {
790                case StanzaError.ERROR_CONDITION_AND_TEXT_NAMESPACE:
791                    switch (name) {
792                    case Stanza.TEXT:
793                        descriptiveTexts = parseDescriptiveTexts(parser, descriptiveTexts);
794                        break;
795                    default:
796                        builder.setCondition(StanzaError.Condition.fromString(name));
797                        String conditionText = parser.nextText();
798                        if (!conditionText.isEmpty()) {
799                            builder.setConditionText(conditionText);
800                        }
801                        break;
802                    }
803                    break;
804                default:
805                    PacketParserUtils.addExtensionElement(extensions, parser, name, namespace, stanzaErrorXmlEnvironment);
806                }
807                break;
808            case END_ELEMENT:
809                if (parser.getDepth() == initialDepth) {
810                    break outerloop;
811                }
812                break;
813            default:
814                // Catch all for incomplete switch (MissingCasesInEnumSwitch) statement.
815                break;
816            }
817        }
818        builder.setExtensions(extensions).setDescriptiveTexts(descriptiveTexts);
819
820        return builder.build();
821    }
822
823    /**
824     * Parses an extension element.
825     *
826     * @param elementName the XML element name of the extension element.
827     * @param namespace the XML namespace of the stanza extension.
828     * @param parser the XML parser, positioned at the starting element of the extension.
829     * @param outerXmlEnvironment the outer XML environment (optional).
830     *
831     * @return an extension element.
832     * @throws XmlPullParserException if an error in the XML parser occurred.
833     * @throws IOException if an I/O error occurred.
834     * @throws SmackParsingException if the Smack parser (provider) encountered invalid input.
835     */
836    public static XmlElement parseExtensionElement(String elementName, String namespace,
837                    XmlPullParser parser, XmlEnvironment outerXmlEnvironment) throws XmlPullParserException, IOException, SmackParsingException {
838        ParserUtils.assertAtStartTag(parser);
839        // See if a provider is registered to handle the extension.
840        ExtensionElementProvider<ExtensionElement> provider = ProviderManager.getExtensionProvider(elementName, namespace);
841        if (provider != null) {
842                return provider.parse(parser, outerXmlEnvironment);
843        }
844
845        // No providers registered, so use a default extension.
846        return StandardExtensionElementProvider.INSTANCE.parse(parser, outerXmlEnvironment);
847    }
848
849    public static StartTls parseStartTlsFeature(XmlPullParser parser)
850                    throws XmlPullParserException, IOException {
851        ParserUtils.assertAtStartTag(parser);
852        assert parser.getNamespace().equals(StartTls.NAMESPACE);
853        int initalDepth = parser.getDepth();
854        boolean required = false;
855        outerloop: while (true) {
856            XmlPullParser.Event event = parser.next();
857            switch (event) {
858            case START_ELEMENT:
859                String name = parser.getName();
860                switch (name) {
861                case "required":
862                    required = true;
863                    break;
864                }
865                break;
866            case END_ELEMENT:
867                if (parser.getDepth() == initalDepth) {
868                    break outerloop;
869                }
870                break;
871            default:
872                // Catch all for incomplete switch (MissingCasesInEnumSwitch) statement.
873                break;
874            }
875        }
876        ParserUtils.assertAtEndTag(parser);
877        return new StartTls(required);
878    }
879
880    public static Session.Feature parseSessionFeature(XmlPullParser parser) throws XmlPullParserException, IOException {
881        ParserUtils.assertAtStartTag(parser);
882        final int initialDepth = parser.getDepth();
883        boolean optional = false;
884
885        outerloop: while (true) {
886            XmlPullParser.Event event = parser.next();
887            switch (event) {
888            case START_ELEMENT:
889                String name = parser.getName();
890                switch (name) {
891                    case Session.Feature.OPTIONAL_ELEMENT:
892                        optional = true;
893                        break;
894                }
895                break;
896            case END_ELEMENT:
897                if (parser.getDepth() == initialDepth) {
898                    break outerloop;
899                }
900                break;
901            default:
902                // Catch all for incomplete switch (MissingCasesInEnumSwitch) statement.
903                break;
904            }
905        }
906
907        return new Session.Feature(optional);
908    }
909
910    public static void addExtensionElement(StanzaBuilder<?> stanzaBuilder, XmlPullParser parser, XmlEnvironment outerXmlEnvironment)
911                    throws XmlPullParserException, IOException, SmackParsingException {
912        ParserUtils.assertAtStartTag(parser);
913        addExtensionElement(stanzaBuilder, parser, parser.getName(), parser.getNamespace(), outerXmlEnvironment);
914    }
915
916    public static void addExtensionElement(StanzaBuilder<?> stanzaBuilder, XmlPullParser parser, String elementName,
917            String namespace, XmlEnvironment outerXmlEnvironment) throws XmlPullParserException, IOException, SmackParsingException {
918        XmlElement extensionElement = parseExtensionElement(elementName, namespace, parser, outerXmlEnvironment);
919        stanzaBuilder.addExtension(extensionElement);
920    }
921
922    public static void addExtensionElement(Stanza packet, XmlPullParser parser, XmlEnvironment outerXmlEnvironment)
923                    throws XmlPullParserException, IOException, SmackParsingException {
924        ParserUtils.assertAtStartTag(parser);
925        addExtensionElement(packet, parser, parser.getName(), parser.getNamespace(), outerXmlEnvironment);
926    }
927
928    public static void addExtensionElement(Stanza packet, XmlPullParser parser, String elementName,
929            String namespace, XmlEnvironment outerXmlEnvironment) throws XmlPullParserException, IOException, SmackParsingException {
930        XmlElement packetExtension = parseExtensionElement(elementName, namespace, parser, outerXmlEnvironment);
931        packet.addExtension(packetExtension);
932    }
933
934    public static void addExtensionElement(Collection<XmlElement> collection, XmlPullParser parser, XmlEnvironment outerXmlEnvironment)
935                    throws XmlPullParserException, IOException, SmackParsingException {
936        addExtensionElement(collection, parser, parser.getName(), parser.getNamespace(), outerXmlEnvironment);
937    }
938
939    public static void addExtensionElement(Collection<XmlElement> collection, XmlPullParser parser,
940                    String elementName, String namespace, XmlEnvironment outerXmlEnvironment) throws XmlPullParserException, IOException, SmackParsingException {
941        XmlElement packetExtension = parseExtensionElement(elementName, namespace, parser, outerXmlEnvironment);
942        collection.add(packetExtension);
943    }
944}