/**
*
* Copyright 2003-2007 Jive Software.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smack.util;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.jivesoftware.smack.SmackConfiguration;
import org.jivesoftware.smack.compress.packet.Compress;
import org.jivesoftware.smack.packet.EmptyResultIQ;
import org.jivesoftware.smack.packet.ErrorIQ;
import org.jivesoftware.smack.packet.ExtensionElement;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.packet.Session;
import org.jivesoftware.smack.packet.Stanza;
import org.jivesoftware.smack.packet.StanzaError;
import org.jivesoftware.smack.packet.StartTls;
import org.jivesoftware.smack.packet.StreamError;
import org.jivesoftware.smack.packet.UnparsedIQ;
import org.jivesoftware.smack.parsing.StandardExtensionElementProvider;
import org.jivesoftware.smack.provider.ExtensionElementProvider;
import org.jivesoftware.smack.provider.IQProvider;
import org.jivesoftware.smack.provider.ProviderManager;
import org.jivesoftware.smack.sasl.packet.SaslStreamElements.SASLFailure;
import org.jxmpp.jid.Jid;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;
/**
* Utility class that helps to parse packets. Any parsing packets method that must be shared
* between many clients must be placed in this utility class.
*
* @author Gaston Dombiak
*/
public class PacketParserUtils {
private static final Logger LOGGER = Logger.getLogger(PacketParserUtils.class.getName());
public static final String FEATURE_XML_ROUNDTRIP = "http://xmlpull.org/v1/doc/features.html#xml-roundtrip";
private static final XmlPullParserFactory XML_PULL_PARSER_FACTORY;
/**
* True if the XmlPullParser supports the XML_ROUNDTRIP feature.
*/
public static final boolean XML_PULL_PARSER_SUPPORTS_ROUNDTRIP;
static {
// Ensure that Smack is initialized.
SmackConfiguration.getVersion();
XmlPullParser xmlPullParser;
boolean roundtrip = false;
try {
XML_PULL_PARSER_FACTORY = XmlPullParserFactory.newInstance();
xmlPullParser = XML_PULL_PARSER_FACTORY.newPullParser();
try {
xmlPullParser.setFeature(FEATURE_XML_ROUNDTRIP, true);
// We could successfully set the feature
roundtrip = true;
} catch (XmlPullParserException e) {
// Doesn't matter if FEATURE_XML_ROUNDTRIP isn't available
LOGGER.log(Level.FINEST, "XmlPullParser does not support XML_ROUNDTRIP", e);
}
}
catch (XmlPullParserException e) {
// Something really bad happened
throw new AssertionError(e);
}
XML_PULL_PARSER_SUPPORTS_ROUNDTRIP = roundtrip;
}
public static XmlPullParser getParserFor(String stanza) throws XmlPullParserException, IOException {
return getParserFor(new StringReader(stanza));
}
public static XmlPullParser getParserFor(Reader reader) throws XmlPullParserException, IOException {
XmlPullParser parser = newXmppParser(reader);
// Wind the parser forward to the first start tag
int event = parser.getEventType();
while (event != XmlPullParser.START_TAG) {
if (event == XmlPullParser.END_DOCUMENT) {
throw new IllegalArgumentException("Document contains no start tag");
}
event = parser.next();
}
return parser;
}
public static XmlPullParser getParserFor(String stanza, String startTag)
throws XmlPullParserException, IOException {
XmlPullParser parser = getParserFor(stanza);
while (true) {
int event = parser.getEventType();
String name = parser.getName();
if (event == XmlPullParser.START_TAG && name.equals(startTag)) {
break;
}
else if (event == XmlPullParser.END_DOCUMENT) {
throw new IllegalArgumentException("Could not find start tag '" + startTag
+ "' in stanza: " + stanza);
}
parser.next();
}
return parser;
}
@SuppressWarnings("unchecked")
public static S parseStanza(String stanza) throws Exception {
return (S) parseStanza(getParserFor(stanza));
}
/**
* Tries to parse and return either a Message, IQ or Presence stanza.
*
* connection is optional and is used to return feature-not-implemented errors for unknown IQ stanzas.
*
* @param parser
* @return a stanza which is either a Message, IQ or Presence.
* @throws Exception
*/
public static Stanza parseStanza(XmlPullParser parser) throws Exception {
ParserUtils.assertAtStartTag(parser);
final String name = parser.getName();
switch (name) {
case Message.ELEMENT:
return parseMessage(parser);
case IQ.IQ_ELEMENT:
return parseIQ(parser);
case Presence.ELEMENT:
return parsePresence(parser);
default:
throw new IllegalArgumentException("Can only parse message, iq or presence, not " + name);
}
}
/**
* Creates a new XmlPullParser suitable for parsing XMPP. This means in particular that
* FEATURE_PROCESS_NAMESPACES is enabled.
*
* Note that not all XmlPullParser implementations will return a String on
* getText()
if the parser is on START_TAG or END_TAG. So you must not rely on this
* behavior when using the parser.
*
* Note that not all XmlPullParser implementations will return a String on
* getText()
if the parser is on START_TAG or END_TAG. So you must not rely on this
* behavior when using the parser.
*
* The parser must be positioned on a START_TAG of an element which MUST NOT contain Mixed * Content (as defined in XML 3.2.2), or else an XmlPullParserException will be thrown. *
* This method is used for the parts where the XMPP specification requires elements that contain * only text or are the empty element. * * @param parser * @return the textual content of the element as String * @throws XmlPullParserException * @throws IOException */ public static String parseElementText(XmlPullParser parser) throws XmlPullParserException, IOException { assert (parser.getEventType() == XmlPullParser.START_TAG); String res; if (parser.isEmptyElementTag()) { res = ""; } else { // Advance to the text of the Element int event = parser.next(); if (event != XmlPullParser.TEXT) { if (event == XmlPullParser.END_TAG) { // Assume this is the end tag of the start tag at the // beginning of this method. Typical examples where this // happens are body elements containing the empty string, // ie. , which appears to be valid XMPP, or a // least it's not explicitly forbidden by RFC 6121 5.2.3 return ""; } else { throw new XmlPullParserException( "Non-empty element tag not followed by text, while Mixed Content (XML 3.2.2) is disallowed"); } } res = parser.getText(); event = parser.next(); if (event != XmlPullParser.END_TAG) { throw new XmlPullParserException( "Non-empty element tag contains child-elements, while Mixed Content (XML 3.2.2) is disallowed"); } } return res; } /** * Returns the current element as string. ** The parser must be positioned on START_TAG. *
* Note that only the outermost namespace attributes ("xmlns") will be returned, not nested ones. * * @param parser the XML pull parser * @return the element as string * @throws XmlPullParserException * @throws IOException */ public static CharSequence parseElement(XmlPullParser parser) throws XmlPullParserException, IOException { return parseElement(parser, false); } public static CharSequence parseElement(XmlPullParser parser, boolean fullNamespaces) throws XmlPullParserException, IOException { assert (parser.getEventType() == XmlPullParser.START_TAG); return parseContentDepth(parser, parser.getDepth(), fullNamespaces); } /** * Returns the content of a element. ** The parser must be positioned on the START_TAG of the element which content is going to get * returned. If the current element is the empty element, then the empty string is returned. If * it is a element which contains just text, then just the text is returned. If it contains * nested elements (and text), then everything from the current opening tag to the corresponding * closing tag of the same depth is returned as String. *
* Note that only the outermost namespace attributes ("xmlns") will be returned, not nested ones. * * @param parser the XML pull parser * @return the content of a tag * @throws XmlPullParserException if parser encounters invalid XML * @throws IOException if an IO error occurs */ public static CharSequence parseContent(XmlPullParser parser) throws XmlPullParserException, IOException { assert (parser.getEventType() == XmlPullParser.START_TAG); if (parser.isEmptyElementTag()) { return ""; } // Advance the parser, since we want to parse the content of the current element parser.next(); return parseContentDepth(parser, parser.getDepth(), false); } public static CharSequence parseContentDepth(XmlPullParser parser, int depth) throws XmlPullParserException, IOException { return parseContentDepth(parser, depth, false); } /** * Returns the content from the current position of the parser up to the closing tag of the * given depth. Note that only the outermost namespace attributes ("xmlns") will be returned, * not nested ones, iffullNamespaces
is false. If it is true, then namespaces of
* parent elements will be added to child elements that don't define a different namespace.
*
* This method is able to parse the content with MX- and KXmlParser. KXmlParser does not support
* xml-roundtrip. i.e. return a String on getText() on START_TAG and END_TAG. We check for the
* XML_ROUNDTRIP feature. If it's not found we are required to work around this limitation, which
* results in only partial support for XML namespaces ("xmlns"): Only the outermost namespace of
* elements will be included in the resulting String, if fullNamespaces
is set to false.
*
* In particular Android's XmlPullParser does not support XML_ROUNDTRIP. *
* * @param parser * @param depth * @param fullNamespaces * @return the content of the current depth * @throws XmlPullParserException * @throws IOException */ public static CharSequence parseContentDepth(XmlPullParser parser, int depth, boolean fullNamespaces) throws XmlPullParserException, IOException { if (parser.getFeature(FEATURE_XML_ROUNDTRIP)) { return parseContentDepthWithRoundtrip(parser, depth, fullNamespaces); } else { return parseContentDepthWithoutRoundtrip(parser, depth, fullNamespaces); } } private static CharSequence parseContentDepthWithoutRoundtrip(XmlPullParser parser, int depth, boolean fullNamespaces) throws XmlPullParserException, IOException { XmlStringBuilder xml = new XmlStringBuilder(); int event = parser.getEventType(); boolean isEmptyElement = false; // XmlPullParser reports namespaces in nested elements even if *only* the outer ones defines // it. This 'flag' ensures that when a namespace is set for an element, it won't be set again // in a nested element. It's an ugly workaround that has the potential to break things. String namespaceElement = null; outerloop: while (true) { switch (event) { case XmlPullParser.START_TAG: xml.halfOpenElement(parser.getName()); if (namespaceElement == null || fullNamespaces) { String namespace = parser.getNamespace(); if (StringUtils.isNotEmpty(namespace)) { xml.attribute("xmlns", namespace); namespaceElement = parser.getName(); } } for (int i = 0; i < parser.getAttributeCount(); i++) { xml.attribute(parser.getAttributeName(i), parser.getAttributeValue(i)); } if (parser.isEmptyElementTag()) { xml.closeEmptyElement(); isEmptyElement = true; } else { xml.rightAngleBracket(); } break; case XmlPullParser.END_TAG: if (isEmptyElement) { // Do nothing as the element was already closed, just reset the flag isEmptyElement = false; } else { xml.closeElement(parser.getName()); } if (namespaceElement != null && namespaceElement.equals(parser.getName())) { // We are on the closing tag, which defined the namespace as starting tag, reset the 'flag' namespaceElement = null; } if (parser.getDepth() <= depth) { // Abort parsing, we are done break outerloop; } break; case XmlPullParser.TEXT: xml.escape(parser.getText()); break; } event = parser.next(); } return xml; } private static CharSequence parseContentDepthWithRoundtrip(XmlPullParser parser, int depth, boolean fullNamespaces) throws XmlPullParserException, IOException { StringBuilder sb = new StringBuilder(); int event = parser.getEventType(); outerloop: while (true) { // Only append the text if the parser is not on on an empty element' start tag. Empty elements are reported // twice, so in order to prevent duplication we only add their text when we are on their end tag. if (!(event == XmlPullParser.START_TAG && parser.isEmptyElementTag())) { CharSequence text = parser.getText(); if (event == XmlPullParser.TEXT) { text = StringUtils.escapeForXmlText(text); } sb.append(text); } if (event == XmlPullParser.END_TAG && parser.getDepth() <= depth) { break outerloop; } event = parser.next(); } return sb; } /** * Parses a presence packet. * * @param parser the XML parser, positioned at the start of a presence packet. * @return a Presence packet. * @throws Exception */ public static Presence parsePresence(XmlPullParser parser) throws Exception { ParserUtils.assertAtStartTag(parser); final int initialDepth = parser.getDepth(); Presence.Type type = Presence.Type.available; String typeString = parser.getAttributeValue("", "type"); if (typeString != null && !typeString.equals("")) { type = Presence.Type.fromString(typeString); } Presence presence = new Presence(type); presence.setTo(ParserUtils.getJidAttribute(parser, "to")); presence.setFrom(ParserUtils.getJidAttribute(parser, "from")); presence.setStanzaId(parser.getAttributeValue("", "id")); String language = ParserUtils.getXmlLang(parser); if (language != null && !"".equals(language.trim())) { // CHECKSTYLE:OFF presence.setLanguage(language); // CHECKSTYLE:ON } // Parse sub-elements outerloop: while (true) { int eventType = parser.next(); switch (eventType) { case XmlPullParser.START_TAG: String elementName = parser.getName(); String namespace = parser.getNamespace(); switch (elementName) { case "status": presence.setStatus(parser.nextText()); break; case "priority": int priority = Integer.parseInt(parser.nextText()); presence.setPriority(priority); break; case "show": String modeText = parser.nextText(); if (StringUtils.isNotEmpty(modeText)) { presence.setMode(Presence.Mode.fromString(modeText)); } else { // Some implementations send presence stanzas with a // '