001/**
002 *
003 * Copyright 2003-2007 Jive Software, 2019 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 {
080        InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
081        return SmackXmlParser.newXmlParser(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    /**
505     * Parses an IQ packet.
506     *
507     * @param parser the XML parser, positioned at the start of an IQ packet.
508     * @param outerXmlEnvironment the outer XML environment (optional).
509     * @return an IQ object.
510     * @throws XmlPullParserException if an error in the XML parser occurred.
511     * @throws XmppStringprepException if the provided string is invalid.
512     * @throws IOException if an I/O error occurred.
513     * @throws SmackParsingException if the Smack parser (provider) encountered invalid input.
514     */
515    public static IQ parseIQ(XmlPullParser parser, XmlEnvironment outerXmlEnvironment) throws XmlPullParserException, XmppStringprepException, IOException, SmackParsingException {
516        ParserUtils.assertAtStartTag(parser);
517        final int initialDepth = parser.getDepth();
518        XmlEnvironment iqXmlEnvironment = XmlEnvironment.from(parser, outerXmlEnvironment);
519        IQ iqPacket = null;
520        StanzaError error = null;
521
522        final String id = parser.getAttributeValue("", "id");
523        IqData iqData = StanzaBuilder.buildIqData(id);
524
525        final Jid to = ParserUtils.getJidAttribute(parser, "to");
526        iqData.to(to);
527
528        final Jid from = ParserUtils.getJidAttribute(parser, "from");
529        iqData.from(from);
530
531        final IQ.Type type = IQ.Type.fromString(parser.getAttributeValue("", "type"));
532        iqData.ofType(type);
533
534        outerloop: while (true) {
535            XmlPullParser.Event eventType = parser.next();
536
537            switch (eventType) {
538            case START_ELEMENT:
539                String elementName = parser.getName();
540                String namespace = parser.getNamespace();
541                switch (elementName) {
542                case "error":
543                    error = PacketParserUtils.parseError(parser, iqXmlEnvironment);
544                    break;
545                // Otherwise, see if there is a registered provider for
546                // this element name and namespace.
547                default:
548                    IqProvider<IQ> provider = ProviderManager.getIQProvider(elementName, namespace);
549                    if (provider != null) {
550                            iqPacket = provider.parse(parser, iqData, outerXmlEnvironment);
551                    }
552                    // Note that if we reach this code, it is guaranteed that the result IQ contained a child element
553                    // (RFC 6120 ยง 8.2.3 6) because otherwise we would have reached the END_ELEMENT first.
554                    else {
555                        // No Provider found for the IQ stanza, parse it to an UnparsedIQ instance
556                        // so that the content of the IQ can be examined later on
557                        iqPacket = new UnparsedIQ(elementName, namespace, parseElement(parser));
558                    }
559                    break;
560                }
561                break;
562            case END_ELEMENT:
563                if (parser.getDepth() == initialDepth) {
564                    break outerloop;
565                }
566                break;
567            default:
568                // Catch all for incomplete switch (MissingCasesInEnumSwitch) statement.
569                break;
570            }
571        }
572        // Decide what to do when an IQ packet was not understood
573        if (iqPacket == null) {
574            switch (type) {
575            case error:
576                // If an IQ packet wasn't created above, create an empty error IQ packet.
577                iqPacket = new ErrorIQ(error);
578                break;
579            case result:
580                iqPacket = new EmptyResultIQ();
581                break;
582            default:
583                break;
584            }
585        }
586
587        // Set basic values on the iq packet.
588        iqPacket.setStanzaId(id);
589        iqPacket.setTo(to);
590        iqPacket.setFrom(from);
591        iqPacket.setType(type);
592        iqPacket.setError(error);
593
594        return iqPacket;
595    }
596
597    /**
598     * Parse the available SASL mechanisms reported from the server.
599     *
600     * @param parser the XML parser, positioned at the start of the mechanisms stanza.
601     * @return a collection of Stings with the mechanisms included in the mechanisms stanza.
602     * @throws IOException if an I/O error occurred.
603     * @throws XmlPullParserException if an error in the XML parser occurred.
604     */
605    public static Collection<String> parseMechanisms(XmlPullParser parser)
606                    throws XmlPullParserException, IOException {
607        List<String> mechanisms = new ArrayList<String>();
608        boolean done = false;
609        while (!done) {
610            XmlPullParser.Event eventType = parser.next();
611
612            if (eventType == XmlPullParser.Event.START_ELEMENT) {
613                String elementName = parser.getName();
614                if (elementName.equals("mechanism")) {
615                    mechanisms.add(parser.nextText());
616                }
617            }
618            else if (eventType == XmlPullParser.Event.END_ELEMENT) {
619                if (parser.getName().equals("mechanisms")) {
620                    done = true;
621                }
622            }
623        }
624        return mechanisms;
625    }
626
627    /**
628     * Parse the Compression Feature reported from the server.
629     *
630     * @param parser the XML parser, positioned at the start of the compression stanza.
631     * @return The CompressionFeature stream element
632     * @throws IOException if an I/O error occurred.
633     * @throws XmlPullParserException if an exception occurs while parsing the stanza.
634     */
635    public static Compress.Feature parseCompressionFeature(XmlPullParser parser)
636                    throws IOException, XmlPullParserException {
637        assert parser.getEventType() == XmlPullParser.Event.START_ELEMENT;
638        String name;
639        final int initialDepth = parser.getDepth();
640        List<String> methods = new LinkedList<>();
641        outerloop: while (true) {
642            XmlPullParser.Event eventType = parser.next();
643            switch (eventType) {
644            case START_ELEMENT:
645                name = parser.getName();
646                switch (name) {
647                case "method":
648                    methods.add(parser.nextText());
649                    break;
650                }
651                break;
652            case END_ELEMENT:
653                name = parser.getName();
654                switch (name) {
655                case Compress.Feature.ELEMENT:
656                    if (parser.getDepth() == initialDepth) {
657                        break outerloop;
658                    }
659                }
660                break;
661            default:
662                // Catch all for incomplete switch (MissingCasesInEnumSwitch) statement.
663                break;
664            }
665        }
666        assert parser.getEventType() == XmlPullParser.Event.END_ELEMENT;
667        assert parser.getDepth() == initialDepth;
668        return new Compress.Feature(methods);
669    }
670
671    public static Map<String, String> parseDescriptiveTexts(XmlPullParser parser, Map<String, String> descriptiveTexts)
672                    throws XmlPullParserException, IOException {
673        if (descriptiveTexts == null) {
674            descriptiveTexts = new HashMap<>();
675        }
676        String xmllang = ParserUtils.getXmlLang(parser);
677        if (xmllang == null) {
678            // XMPPError assumes the default locale, 'en', or the empty string.
679            // Establish the invariant that there is never null as a key.
680            xmllang = "";
681        }
682
683        String text = parser.nextText();
684        String previousValue = descriptiveTexts.put(xmllang, text);
685        assert previousValue == null;
686        return descriptiveTexts;
687    }
688
689    public static StreamError parseStreamError(XmlPullParser parser) throws XmlPullParserException, IOException, SmackParsingException {
690        return parseStreamError(parser, null);
691    }
692
693    /**
694     * Parses stream error packets.
695     *
696     * @param parser the XML parser.
697     * @param outerXmlEnvironment the outer XML environment (optional).
698     * @return an stream error packet.
699     * @throws IOException if an I/O error occurred.
700     * @throws XmlPullParserException if an error in the XML parser occurred.
701     * @throws SmackParsingException if the Smack parser (provider) encountered invalid input.
702     */
703    public static StreamError parseStreamError(XmlPullParser parser, XmlEnvironment outerXmlEnvironment) throws XmlPullParserException, IOException, SmackParsingException {
704        final int initialDepth = parser.getDepth();
705        List<XmlElement> extensions = new ArrayList<>();
706        Map<String, String> descriptiveTexts = null;
707        StreamError.Condition condition = null;
708        String conditionText = null;
709        XmlEnvironment streamErrorXmlEnvironment = XmlEnvironment.from(parser, outerXmlEnvironment);
710        outerloop: while (true) {
711            XmlPullParser.Event eventType = parser.next();
712            switch (eventType) {
713            case START_ELEMENT:
714                String name = parser.getName();
715                String namespace = parser.getNamespace();
716                switch (namespace) {
717                case StreamError.NAMESPACE:
718                    switch (name) {
719                    case "text":
720                        descriptiveTexts = parseDescriptiveTexts(parser, descriptiveTexts);
721                        break;
722                    default:
723                        // If it's not a text element, that is qualified by the StreamError.NAMESPACE,
724                        // then it has to be the stream error code
725                        condition = StreamError.Condition.fromString(name);
726                        conditionText = parser.nextText();
727                        if (conditionText.isEmpty()) {
728                            conditionText = null;
729                        }
730                        break;
731                    }
732                    break;
733                default:
734                    PacketParserUtils.addExtensionElement(extensions, parser, name, namespace, streamErrorXmlEnvironment);
735                    break;
736                }
737                break;
738            case END_ELEMENT:
739                if (parser.getDepth() == initialDepth) {
740                    break outerloop;
741                }
742                break;
743            default:
744                // Catch all for incomplete switch (MissingCasesInEnumSwitch) statement.
745                break;
746            }
747        }
748        return new StreamError(condition, conditionText, descriptiveTexts, extensions);
749    }
750
751    public static StanzaError parseError(XmlPullParser parser) throws XmlPullParserException, IOException, SmackParsingException {
752        return parseError(parser, null);
753    }
754
755    /**
756     * Parses error sub-packets.
757     *
758     * @param parser the XML parser.
759     * @param outerXmlEnvironment the outer XML environment (optional).
760     * @return an error sub-packet.
761     * @throws IOException if an I/O error occurred.
762     * @throws XmlPullParserException if an error in the XML parser occurred.
763     * @throws SmackParsingException if the Smack parser (provider) encountered invalid input.
764     */
765    public static StanzaError parseError(XmlPullParser parser, XmlEnvironment outerXmlEnvironment) throws XmlPullParserException, IOException, SmackParsingException {
766        final int initialDepth = parser.getDepth();
767        Map<String, String> descriptiveTexts = null;
768        XmlEnvironment stanzaErrorXmlEnvironment = XmlEnvironment.from(parser, outerXmlEnvironment);
769        List<XmlElement> extensions = new ArrayList<>();
770        StanzaError.Builder builder = StanzaError.getBuilder();
771
772        // Parse the error header
773        builder.setType(StanzaError.Type.fromString(parser.getAttributeValue("", "type")));
774        builder.setErrorGenerator(parser.getAttributeValue("", "by"));
775
776        outerloop: while (true) {
777            XmlPullParser.Event eventType = parser.next();
778            switch (eventType) {
779            case START_ELEMENT:
780                String name = parser.getName();
781                String namespace = parser.getNamespace();
782                switch (namespace) {
783                case StanzaError.ERROR_CONDITION_AND_TEXT_NAMESPACE:
784                    switch (name) {
785                    case Stanza.TEXT:
786                        descriptiveTexts = parseDescriptiveTexts(parser, descriptiveTexts);
787                        break;
788                    default:
789                        builder.setCondition(StanzaError.Condition.fromString(name));
790                        String conditionText = parser.nextText();
791                        if (!conditionText.isEmpty()) {
792                            builder.setConditionText(conditionText);
793                        }
794                        break;
795                    }
796                    break;
797                default:
798                    PacketParserUtils.addExtensionElement(extensions, parser, name, namespace, stanzaErrorXmlEnvironment);
799                }
800                break;
801            case END_ELEMENT:
802                if (parser.getDepth() == initialDepth) {
803                    break outerloop;
804                }
805                break;
806            default:
807                // Catch all for incomplete switch (MissingCasesInEnumSwitch) statement.
808                break;
809            }
810        }
811        builder.setExtensions(extensions).setDescriptiveTexts(descriptiveTexts);
812
813        return builder.build();
814    }
815
816    /**
817     * Parses an extension element.
818     *
819     * @param elementName the XML element name of the extension element.
820     * @param namespace the XML namespace of the stanza extension.
821     * @param parser the XML parser, positioned at the starting element of the extension.
822     * @param outerXmlEnvironment the outer XML environment (optional).
823     *
824     * @return an extension element.
825     * @throws XmlPullParserException if an error in the XML parser occurred.
826     * @throws IOException if an I/O error occurred.
827     * @throws SmackParsingException if the Smack parser (provider) encountered invalid input.
828     */
829    public static XmlElement parseExtensionElement(String elementName, String namespace,
830                    XmlPullParser parser, XmlEnvironment outerXmlEnvironment) throws XmlPullParserException, IOException, SmackParsingException {
831        ParserUtils.assertAtStartTag(parser);
832        // See if a provider is registered to handle the extension.
833        ExtensionElementProvider<ExtensionElement> provider = ProviderManager.getExtensionProvider(elementName, namespace);
834        if (provider != null) {
835                return provider.parse(parser, outerXmlEnvironment);
836        }
837
838        // No providers registered, so use a default extension.
839        return StandardExtensionElementProvider.INSTANCE.parse(parser, outerXmlEnvironment);
840    }
841
842    public static StartTls parseStartTlsFeature(XmlPullParser parser)
843                    throws XmlPullParserException, IOException {
844        ParserUtils.assertAtStartTag(parser);
845        assert parser.getNamespace().equals(StartTls.NAMESPACE);
846        int initalDepth = parser.getDepth();
847        boolean required = false;
848        outerloop: while (true) {
849            XmlPullParser.Event event = parser.next();
850            switch (event) {
851            case START_ELEMENT:
852                String name = parser.getName();
853                switch (name) {
854                case "required":
855                    required = true;
856                    break;
857                }
858                break;
859            case END_ELEMENT:
860                if (parser.getDepth() == initalDepth) {
861                    break outerloop;
862                }
863                break;
864            default:
865                // Catch all for incomplete switch (MissingCasesInEnumSwitch) statement.
866                break;
867            }
868        }
869        ParserUtils.assertAtEndTag(parser);
870        return new StartTls(required);
871    }
872
873    public static Session.Feature parseSessionFeature(XmlPullParser parser) throws XmlPullParserException, IOException {
874        ParserUtils.assertAtStartTag(parser);
875        final int initialDepth = parser.getDepth();
876        boolean optional = false;
877
878        outerloop: while (true) {
879            XmlPullParser.Event event = parser.next();
880            switch (event) {
881            case START_ELEMENT:
882                String name = parser.getName();
883                switch (name) {
884                    case Session.Feature.OPTIONAL_ELEMENT:
885                        optional = true;
886                        break;
887                }
888                break;
889            case END_ELEMENT:
890                if (parser.getDepth() == initialDepth) {
891                    break outerloop;
892                }
893                break;
894            default:
895                // Catch all for incomplete switch (MissingCasesInEnumSwitch) statement.
896                break;
897            }
898        }
899
900        return new Session.Feature(optional);
901    }
902
903    public static void addExtensionElement(StanzaBuilder<?> stanzaBuilder, XmlPullParser parser, XmlEnvironment outerXmlEnvironment)
904                    throws XmlPullParserException, IOException, SmackParsingException {
905        ParserUtils.assertAtStartTag(parser);
906        addExtensionElement(stanzaBuilder, parser, parser.getName(), parser.getNamespace(), outerXmlEnvironment);
907    }
908
909    public static void addExtensionElement(StanzaBuilder<?> stanzaBuilder, XmlPullParser parser, String elementName,
910            String namespace, XmlEnvironment outerXmlEnvironment) throws XmlPullParserException, IOException, SmackParsingException {
911        XmlElement extensionElement = parseExtensionElement(elementName, namespace, parser, outerXmlEnvironment);
912        stanzaBuilder.addExtension(extensionElement);
913    }
914
915    public static void addExtensionElement(Stanza packet, XmlPullParser parser, XmlEnvironment outerXmlEnvironment)
916                    throws XmlPullParserException, IOException, SmackParsingException {
917        ParserUtils.assertAtStartTag(parser);
918        addExtensionElement(packet, parser, parser.getName(), parser.getNamespace(), outerXmlEnvironment);
919    }
920
921    public static void addExtensionElement(Stanza packet, XmlPullParser parser, String elementName,
922            String namespace, XmlEnvironment outerXmlEnvironment) throws XmlPullParserException, IOException, SmackParsingException {
923        XmlElement packetExtension = parseExtensionElement(elementName, namespace, parser, outerXmlEnvironment);
924        packet.addExtension(packetExtension);
925    }
926
927    public static void addExtensionElement(Collection<XmlElement> collection, XmlPullParser parser, XmlEnvironment outerXmlEnvironment)
928                    throws XmlPullParserException, IOException, SmackParsingException {
929        addExtensionElement(collection, parser, parser.getName(), parser.getNamespace(), outerXmlEnvironment);
930    }
931
932    public static void addExtensionElement(Collection<XmlElement> collection, XmlPullParser parser,
933                    String elementName, String namespace, XmlEnvironment outerXmlEnvironment) throws XmlPullParserException, IOException, SmackParsingException {
934        XmlElement packetExtension = parseExtensionElement(elementName, namespace, parser, outerXmlEnvironment);
935        collection.add(packetExtension);
936    }
937}