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