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