From 30bc5afa2bc86e249f2c4b891c2e15e945f471f9 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Sat, 22 Jun 2013 17:01:40 +0000 Subject: [PATCH] SMACK-425 Introduced smack.parsing.ParsingExceptionCallback, a callback invoked when a exception is thrown while parsing a stanza. Smack is now able to either rethrow the exception ulitmatly causing a disconnect *or* log/ignore the exception and resume parsing after the faulty stanza. git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/branches/smack_3_3_1@13688 b35dd754-fafc-0310-a699-88a17e54d16e --- .../eclipse/settings/org.eclipse.jdt.ui.prefs | 3 +- .../org/jivesoftware/smack/PacketReader.java | 45 ++++++++++- .../smack/SmackConfiguration.java | 28 +++++++ .../jivesoftware/smack/XMPPConnection.java | 22 ++++++ .../smack/parsing/LogException.java | 51 +++++++++++++ .../parsing/ParsingExceptionCallback.java | 76 +++++++++++++++++++ .../smack/parsing/ThrowException.java | 48 ++++++++++++ .../smack/parsing/UnparsedIQ.java | 62 +++++++++++++++ .../smack/parsing/UnparsedMessage.java | 57 ++++++++++++++ .../smack/parsing/UnparsedPresence.java | 61 +++++++++++++++ .../smack/util/PacketParserUtils.java | 9 ++- .../smack/parsing/ParsingExceptionTest.java | 70 +++++++++++++++++ 12 files changed, 524 insertions(+), 8 deletions(-) create mode 100644 source/org/jivesoftware/smack/parsing/LogException.java create mode 100644 source/org/jivesoftware/smack/parsing/ParsingExceptionCallback.java create mode 100644 source/org/jivesoftware/smack/parsing/ThrowException.java create mode 100644 source/org/jivesoftware/smack/parsing/UnparsedIQ.java create mode 100644 source/org/jivesoftware/smack/parsing/UnparsedMessage.java create mode 100644 source/org/jivesoftware/smack/parsing/UnparsedPresence.java create mode 100644 test-unit/org/jivesoftware/smack/parsing/ParsingExceptionTest.java diff --git a/build/eclipse/settings/org.eclipse.jdt.ui.prefs b/build/eclipse/settings/org.eclipse.jdt.ui.prefs index fcdbc532c..dde05c864 100644 --- a/build/eclipse/settings/org.eclipse.jdt.ui.prefs +++ b/build/eclipse/settings/org.eclipse.jdt.ui.prefs @@ -1,4 +1,3 @@ -#Tue Jan 29 23:27:16 CET 2013 eclipse.preferences.version=1 editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true formatter_profile=_ignite @@ -37,7 +36,7 @@ sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class= sp_cleanup.qualify_static_member_accesses_with_declaring_class=false sp_cleanup.qualify_static_method_accesses_with_declaring_class=false sp_cleanup.remove_private_constructors=true -sp_cleanup.remove_trailing_whitespaces=false +sp_cleanup.remove_trailing_whitespaces=true sp_cleanup.remove_trailing_whitespaces_all=true sp_cleanup.remove_trailing_whitespaces_ignore_empty=false sp_cleanup.remove_unnecessary_casts=true diff --git a/source/org/jivesoftware/smack/PacketReader.java b/source/org/jivesoftware/smack/PacketReader.java index 826360cf3..05f8de99c 100644 --- a/source/org/jivesoftware/smack/PacketReader.java +++ b/source/org/jivesoftware/smack/PacketReader.java @@ -22,6 +22,10 @@ package org.jivesoftware.smack; import org.jivesoftware.smack.Connection.ListenerWrapper; import org.jivesoftware.smack.packet.*; +import org.jivesoftware.smack.parsing.ParsingExceptionCallback; +import org.jivesoftware.smack.parsing.UnparsedIQ; +import org.jivesoftware.smack.parsing.UnparsedMessage; +import org.jivesoftware.smack.parsing.UnparsedPresence; import org.jivesoftware.smack.sasl.SASLMechanism.Challenge; import org.jivesoftware.smack.sasl.SASLMechanism.Failure; import org.jivesoftware.smack.sasl.SASLMechanism.Success; @@ -176,14 +180,49 @@ class PacketReader { int eventType = parser.getEventType(); do { if (eventType == XmlPullParser.START_TAG) { + int parserDepth = parser.getDepth(); + ParsingExceptionCallback callback = connection.getParsingExceptionCallback(); if (parser.getName().equals("message")) { - processPacket(PacketParserUtils.parseMessage(parser)); + Packet packet; + try { + packet = PacketParserUtils.parseMessage(parser); + } catch (Exception e) { + String content = PacketParserUtils.parseContentDepth(parser, parserDepth); + UnparsedMessage message = new UnparsedMessage(content, e); + if (callback != null) { + callback.messageParsingException(e, message); + } + continue; + } + processPacket(packet); } else if (parser.getName().equals("iq")) { - processPacket(PacketParserUtils.parseIQ(parser, connection)); + IQ iq; + try { + iq = PacketParserUtils.parseIQ(parser, connection); + } catch (Exception e) { + String content = PacketParserUtils.parseContentDepth(parser, parserDepth); + UnparsedIQ uniq = new UnparsedIQ(content, e); + if (callback != null) { + callback.iqParsingException(e, uniq); + } + continue; + } + processPacket(iq); } else if (parser.getName().equals("presence")) { - processPacket(PacketParserUtils.parsePresence(parser)); + Presence presence; + try { + presence = PacketParserUtils.parsePresence(parser); + } catch (Exception e) { + String content = PacketParserUtils.parseContentDepth(parser, parserDepth); + UnparsedPresence unpresence = new UnparsedPresence(content, e); + if (callback != null) { + callback.presenceParsingException(e, unpresence); + } + continue; + } + processPacket(presence); } // We found an opening stream. Record information about it, then notify // the connectionID lock so that the packet reader startup can finish. diff --git a/source/org/jivesoftware/smack/SmackConfiguration.java b/source/org/jivesoftware/smack/SmackConfiguration.java index aae8184bc..eb8069381 100644 --- a/source/org/jivesoftware/smack/SmackConfiguration.java +++ b/source/org/jivesoftware/smack/SmackConfiguration.java @@ -28,6 +28,8 @@ import java.util.Enumeration; import java.util.List; import java.util.Vector; +import org.jivesoftware.smack.parsing.ParsingExceptionCallback; +import org.jivesoftware.smack.parsing.ThrowException; import org.xmlpull.mxp1.MXParser; import org.xmlpull.v1.XmlPullParser; @@ -58,6 +60,12 @@ public final class SmackConfiguration { private static int localSocks5ProxyPort = 7777; private static int packetCollectorSize = 5000; + /** + * The default parsing exception callback is {@link ThrowException} which will + * throw an exception and therefore disconnect the active connection. + */ + private static ParsingExceptionCallback defaultCallback = new ThrowException(); + /** * This automatically enables EntityCaps for new connections if it is set to true */ @@ -328,6 +336,26 @@ public final class SmackConfiguration { autoEnableEntityCaps = b; } + /** + * Set the default parsing exception callback for all newly created connections + * + * @param callback + * @see ParsingExceptionCallback + */ + public static void setDefaultParsingExceptionCallback(ParsingExceptionCallback callback) { + defaultCallback = callback; + } + + /** + * Returns the default parsing exception callback + * + * @return the default parsing exception callback + * @see ParsingExceptionCallback + */ + public static ParsingExceptionCallback getDefaultParsingExceptionCallback() { + return defaultCallback; + } + private static void parseClassToLoad(XmlPullParser parser) throws Exception { String className = parser.nextText(); // Attempt to load the class so that the class can get initialized diff --git a/source/org/jivesoftware/smack/XMPPConnection.java b/source/org/jivesoftware/smack/XMPPConnection.java index 1b2dedad0..f0b6eafd8 100644 --- a/source/org/jivesoftware/smack/XMPPConnection.java +++ b/source/org/jivesoftware/smack/XMPPConnection.java @@ -25,6 +25,7 @@ import org.jivesoftware.smack.filter.PacketFilter; import org.jivesoftware.smack.packet.Packet; import org.jivesoftware.smack.packet.Presence; import org.jivesoftware.smack.packet.XMPPError; +import org.jivesoftware.smack.parsing.ParsingExceptionCallback; import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.smack.util.dns.HostAddress; @@ -89,6 +90,8 @@ public class XMPPConnection extends Connection { private boolean anonymous = false; private boolean usingTLS = false; + private ParsingExceptionCallback parsingExceptionCallback = SmackConfiguration.getDefaultParsingExceptionCallback(); + PacketWriter packetWriter; PacketReader packetReader; @@ -202,6 +205,25 @@ public class XMPPConnection extends Connection { return user; } + /** + * Install a parsing exception callback, which will be invoked once an exception is encountered while parsing a + * stanza + * + * @param callback the callback to install + */ + public void setParsingExceptionCallback(ParsingExceptionCallback callback) { + parsingExceptionCallback = callback; + } + + /** + * Get the current active parsing exception callback. + * + * @return the active exception callback or null if there is none + */ + public ParsingExceptionCallback getParsingExceptionCallback() { + return parsingExceptionCallback; + } + @Override public synchronized void login(String username, String password, String resource) throws XMPPException { if (!isConnected()) { diff --git a/source/org/jivesoftware/smack/parsing/LogException.java b/source/org/jivesoftware/smack/parsing/LogException.java new file mode 100644 index 000000000..dd3031e9a --- /dev/null +++ b/source/org/jivesoftware/smack/parsing/LogException.java @@ -0,0 +1,51 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2013 Florian Schmaus. + * + * All rights reserved. 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.parsing; + +/** + * Simple parsing exception callback that only logs the encountered parsing exception to stderr. + * + * @author Florian Schmaus + * + */ +public class LogException extends ParsingExceptionCallback { + + @Override + public void messageParsingException(Exception e, UnparsedMessage message) throws Exception { + System.err.print("Smack message parsing exception: " + e.getMessage()); + e.printStackTrace(); + System.err.println("Unparsed content: " + message.getContent()); + } + + @Override + public void iqParsingException(Exception e, UnparsedIQ iq) throws Exception { + System.err.print("Smack iq parsing exception: " + e.getMessage()); + e.printStackTrace(); + System.err.println("Unparsed content: " + iq.getContent()); + } + + @Override + public void presenceParsingException(Exception e, UnparsedPresence presence) throws Exception { + System.err.print("Smack presence parsing exception: " + e.getMessage()); + e.printStackTrace(); + System.err.println("Unparsed content: " + presence.getContent()); + } +} diff --git a/source/org/jivesoftware/smack/parsing/ParsingExceptionCallback.java b/source/org/jivesoftware/smack/parsing/ParsingExceptionCallback.java new file mode 100644 index 000000000..3c57d089c --- /dev/null +++ b/source/org/jivesoftware/smack/parsing/ParsingExceptionCallback.java @@ -0,0 +1,76 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2013 Florian Schmaus. + * + * All rights reserved. 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.parsing; + +/** + * Base class to receive parsing exceptions. + * + * If this class is used as callback, then Smack will silently ignore the stanza that caused the parsing exception and + * place the parser after the faulty stanza. + * + * Subclasses may or may not override certain methods of this class. Each of these methods will receive the exception + * that caused the parsing error and an instance of an Unparsed Packet type. The latter can be used to inspect the + * stanza that caused the parsing error by using the getContent() (for example {@link UnparsedIQ#getContent()}) + * method. + * + * Smack provides 2 predefined ParsingExceptionCallback's: {@link LogException} and {@link ThrowException}. + * + * @author Florian Schmaus + * + */ +public abstract class ParsingExceptionCallback { + + /** + * Called when parsing an message stanza caused an exception. + * + * @param e + * the exception thrown while parsing the message stanza + * @param message + * the raw message stanza data that caused the exception + * @throws Exception + */ + public void messageParsingException(Exception e, UnparsedMessage message) throws Exception { + } + + /** + * Called when parsing an IQ stanza caused an exception. + * + * @param e + * the exception thrown while parsing the iq stanza + * @param iq + * the raw iq stanza data that caused the exception + * @throws Exception + */ + public void iqParsingException(Exception e, UnparsedIQ iq) throws Exception { + } + + /** + * Called when parsing a presence stanza caused an exception. + * + * @param e + * the exception thrown while parsing the presence stanza + * @param presence + * the raw presence stanza data that caused the exception + * @throws Exception + */ + public void presenceParsingException(Exception e, UnparsedPresence presence) throws Exception { + } +} diff --git a/source/org/jivesoftware/smack/parsing/ThrowException.java b/source/org/jivesoftware/smack/parsing/ThrowException.java new file mode 100644 index 000000000..400950346 --- /dev/null +++ b/source/org/jivesoftware/smack/parsing/ThrowException.java @@ -0,0 +1,48 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2013 Florian Schmaus. + * + * All rights reserved. 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.parsing; + +import org.jivesoftware.smack.ConnectionListener; + +/** + * Parsing exception callback class that simply throws the encountered parsing exception. This usually leads to an + * {@link ConnectionListener#connectionClosedOnError(Exception)} disconnect of the connection. + * + * @author Florian Schmaus + * + */ +public class ThrowException extends ParsingExceptionCallback { + + @Override + public void messageParsingException(Exception e, UnparsedMessage message) throws Exception { + throw e; + } + + @Override + public void iqParsingException(Exception e, UnparsedIQ iq) throws Exception { + throw e; + } + + @Override + public void presenceParsingException(Exception e, UnparsedPresence presence) throws Exception { + throw e; + } +} diff --git a/source/org/jivesoftware/smack/parsing/UnparsedIQ.java b/source/org/jivesoftware/smack/parsing/UnparsedIQ.java new file mode 100644 index 000000000..a9ad6a6d5 --- /dev/null +++ b/source/org/jivesoftware/smack/parsing/UnparsedIQ.java @@ -0,0 +1,62 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2013 Florian Schmaus. + * + * All rights reserved. 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.parsing; + +import org.jivesoftware.smack.packet.IQ; + +/** + * Representation of an unparsed IQ stanza. + * + * @author Florian Schmaus + * + */ +public class UnparsedIQ extends IQ { + private final String content; + private final Exception e; + + public UnparsedIQ(final String content, final Exception e) { + this.content = content; + this.e = e; + } + + /** + * + * @return the exception that caused the parser to fail + */ + public Exception getException() { + return e; + } + + /** + * Retrieve the raw stanza data + * + * @return the raw stanza data + */ + public String getContent() { + return content; + } + + @Override + public String getChildElementXML() { + return null; + } + +} diff --git a/source/org/jivesoftware/smack/parsing/UnparsedMessage.java b/source/org/jivesoftware/smack/parsing/UnparsedMessage.java new file mode 100644 index 000000000..e23773cf2 --- /dev/null +++ b/source/org/jivesoftware/smack/parsing/UnparsedMessage.java @@ -0,0 +1,57 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2013 Florian Schmaus. + * + * All rights reserved. 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.parsing; + +import org.jivesoftware.smack.packet.Message; + +/** + * Representation of an unparsed IQ stanza. + * + * @author Florian Schmaus + * + */ +public class UnparsedMessage extends Message { + private final String content; + private final Exception e; + + public UnparsedMessage(final String content, final Exception e) { + this.content = content; + this.e = e; + } + + /** + * + * @return the exception that caused the parser to fail + */ + public Exception getException() { + return e; + } + + /** + * Retrieve the raw stanza data + * + * @return the raw stanza data + */ + public String getContent() { + return content; + } + +} diff --git a/source/org/jivesoftware/smack/parsing/UnparsedPresence.java b/source/org/jivesoftware/smack/parsing/UnparsedPresence.java new file mode 100644 index 000000000..a14e0ae49 --- /dev/null +++ b/source/org/jivesoftware/smack/parsing/UnparsedPresence.java @@ -0,0 +1,61 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2013 Florian Schmaus. + * + * All rights reserved. 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.parsing; + +import org.jivesoftware.smack.packet.Presence; + +/** + * Representation of an unparsed IQ stanza. + * + * @author Florian Schmaus + * + */ +public class UnparsedPresence extends Presence { + private String content; + private Exception e; + + public UnparsedPresence(Type type) { + super(type); + } + + public UnparsedPresence(final String content, final Exception e) { + super(Presence.Type.error); + this.content = content; + this.e = e; + } + + /** + * + * @return the exception that caused the parser to fail + */ + public Exception getException() { + return e; + } + + /** + * Retrieve the raw stanza data + * + * @return the raw stanza data + */ + public String getContent() { + return content; + } +} diff --git a/source/org/jivesoftware/smack/util/PacketParserUtils.java b/source/org/jivesoftware/smack/util/PacketParserUtils.java index a574be3db..57d539c59 100644 --- a/source/org/jivesoftware/smack/util/PacketParserUtils.java +++ b/source/org/jivesoftware/smack/util/PacketParserUtils.java @@ -171,10 +171,13 @@ public class PacketParserUtils { */ private static String parseContent(XmlPullParser parser) throws XmlPullParserException, IOException { - StringBuffer content = new StringBuffer(); int parserDepth = parser.getDepth(); - while (!(parser.next() == XmlPullParser.END_TAG && parser - .getDepth() == parserDepth)) { + return parseContentDepth(parser, parserDepth); + } + + public static String parseContentDepth(XmlPullParser parser, int depth) throws XmlPullParserException, IOException { + StringBuffer content = new StringBuffer(); + while (!(parser.next() == XmlPullParser.END_TAG && parser.getDepth() == depth)) { content.append(parser.getText()); } return content.toString(); diff --git a/test-unit/org/jivesoftware/smack/parsing/ParsingExceptionTest.java b/test-unit/org/jivesoftware/smack/parsing/ParsingExceptionTest.java new file mode 100644 index 000000000..fd674ae0c --- /dev/null +++ b/test-unit/org/jivesoftware/smack/parsing/ParsingExceptionTest.java @@ -0,0 +1,70 @@ +package org.jivesoftware.smack.parsing; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import org.jivesoftware.smack.TestUtils; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smack.provider.PacketExtensionProvider; +import org.jivesoftware.smack.provider.ProviderManager; +import org.jivesoftware.smack.util.PacketParserUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.xmlpull.v1.XmlPullParser; + +public class ParsingExceptionTest { + private final static ProviderManager PM = ProviderManager.getInstance(); + + private final static String EXTENSION2 = + "" + + "" + + "" + + "" + + "" + + ""; + + @Before + public void init() { + PM.addExtensionProvider(ThrowException.ELEMENT, ThrowException.NAMESPACE, new ThrowException()); + } + + @After + public void tini() { + PM.removeExtensionProvider(ThrowException.ELEMENT, ThrowException.NAMESPACE); + } + + @Test + public void consumeUnparsedInput() throws Exception { + XmlPullParser parser = TestUtils.getMessageParser( + "" + + "<" + ThrowException.ELEMENT + " xmlns='" + ThrowException.NAMESPACE + "'>" + + "" + + "" + + "" + + EXTENSION2 + + ""); + int parserDepth = parser.getDepth(); + String content = null; + try { + PacketParserUtils.parseMessage(parser); + } catch (Exception e) { + content = PacketParserUtils.parseContentDepth(parser, parserDepth); + } + assertNotNull(content); + assertEquals(content, "" + "" + EXTENSION2); + + } + + static class ThrowException implements PacketExtensionProvider { + public static final String ELEMENT = "exception"; + public static final String NAMESPACE = "http://smack.jivesoftware.org/exception"; + + @Override + public PacketExtension parseExtension(XmlPullParser parser) throws Exception { + throw new XMPPException("Test Exception"); + } + + } +}