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}