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