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