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