From fc51f3df483d9277a4085069e80662ea5ea76560 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Thu, 11 Sep 2014 09:49:16 +0200 Subject: [PATCH] Add support for XEP-0198: Stream Management - De-duplicate code by moving it into AbstractXMPPConnection - Introduce TopLevelStreamElement as superclass for all XMPP stream elements. - Add SynchronizationPoint, ParserUtils - Add ParserUtils Fixes SMACK-333 and SMACK-521 --- build.gradle | 2 +- documentation/extensions/index.md | 8 + documentation/extensions/streammanagement.md | 37 + resources/getCopyright.sh | 25 + .../smack/bosh/BOSHPacketReader.java | 45 +- .../smack/bosh/XMPPBOSHConnection.java | 110 +- .../smack/AbstractXMPPConnection.java | 333 +++-- .../java/org/jivesoftware/smack/Roster.java | 31 +- .../smack/SASLAuthentication.java | 45 +- .../jivesoftware/smack/SmackException.java | 15 + .../smack/SynchronizationPoint.java | 190 +++ .../jivesoftware/smack/XMPPConnection.java | 49 +- .../org/jivesoftware/smack/XMPPException.java | 1 - .../smack/compress/packet/Compress.java | 90 ++ .../smack/compress/packet/Compressed.java | 45 + .../Java7ZlibInputOutputStream.java | 1 + .../smack/debugger/ConsoleDebugger.java | 5 +- .../org/jivesoftware/smack/packet/Bind.java | 46 +- .../smack}/packet/CapsExtension.java | 36 +- .../jivesoftware/smack/packet/Element.java | 9 +- .../smack/packet/FullStreamElement.java | 28 + .../org/jivesoftware/smack/packet/IQ.java | 5 +- .../jivesoftware/smack/packet/Mechanisms.java | 66 + .../jivesoftware/smack/packet/Message.java | 5 +- .../smack/packet/NamedElement.java | 33 + .../org/jivesoftware/smack/packet/Packet.java | 11 +- .../smack/packet/PacketExtension.java | 2 +- .../smack/packet/PlainStreamElement.java | 34 + .../smack/packet/Registration.java | 34 +- .../jivesoftware/smack/packet/RosterVer.java | 48 + .../jivesoftware/smack/packet/Session.java | 32 +- .../jivesoftware/smack/packet/StartTls.java | 59 + .../jivesoftware/smack/packet/StreamOpen.java | 57 + .../smack/packet/TopLevelStreamElement.java | 26 + .../jivesoftware/smack/packet/XMPPError.java | 4 + .../smack/provider/ProviderManager.java | 28 +- .../smack/sasl/SASLErrorException.java | 2 +- .../smack/sasl/SASLMechanism.java | 8 +- ...slStanzas.java => SaslStreamElements.java} | 24 +- .../org/jivesoftware/smack/util/DNSUtil.java | 21 +- .../smack/util/PacketParserUtils.java | 166 ++- .../jivesoftware/smack/util/ParserUtils.java | 96 ++ .../smack/util/XmlStringBuilder.java | 5 +- .../jivesoftware/smack/DummyConnection.java | 17 +- .../smack/RosterVersioningTest.java | 2 - .../smack/test/util/TestUtils.java | 3 +- .../bytestreams/socks5/packet/Bytestream.java | 8 +- .../smackx/caps/EntityCapsManager.java | 54 +- .../caps/provider/CapsExtensionProvider.java | 2 +- .../provider/DelayInformationProvider.java | 1 - .../smackx/muc/packet/Destroy.java | 4 +- .../smackx/muc/packet/MUCInitialPresence.java | 4 +- .../smackx/muc/packet/MUCItem.java | 4 +- .../smackx/muc/packet/MUCUser.java | 8 +- .../delay/provider/DelayInformationTest.java | 16 +- .../smack/tcp/XMPPTCPConnection.java | 1315 ++++++++++------- .../jivesoftware/smack/tcp/sm/SMUtils.java | 50 + .../tcp/sm/StreamManagementException.java | 56 + .../smack/tcp/sm/packet/StreamManagement.java | 344 +++++ .../tcp/sm/predicates/AfterXStanzas.java | 45 + .../tcp/sm/predicates/ForEveryMessage.java | 38 + .../tcp/sm/predicates/ForEveryStanza.java | 34 + .../ForMatchingPredicateOrAfterXStanzas.java | 40 + .../tcp/sm/predicates/OnceForThisStanza.java | 55 + .../smack/tcp/sm/predicates/Predicate.java | 30 + .../tcp/sm/predicates/ShortcutPredicates.java | 54 + .../sm/provider/ParseStreamManagement.java | 88 ++ .../smack/tcp/PacketWriterTest.java | 17 +- .../provider/ParseStreamManagementTest.java | 154 ++ 69 files changed, 3277 insertions(+), 1083 deletions(-) create mode 100644 documentation/extensions/streammanagement.md create mode 100755 resources/getCopyright.sh create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/SynchronizationPoint.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/compress/packet/Compress.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/compress/packet/Compressed.java rename {smack-extensions/src/main/java/org/jivesoftware/smackx/caps => smack-core/src/main/java/org/jivesoftware/smack}/packet/CapsExtension.java (63%) create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/packet/FullStreamElement.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/packet/Mechanisms.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/packet/NamedElement.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/packet/PlainStreamElement.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/packet/RosterVer.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/packet/StartTls.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/packet/StreamOpen.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/packet/TopLevelStreamElement.java rename smack-core/src/main/java/org/jivesoftware/smack/sasl/packet/{SaslStanzas.java => SaslStreamElements.java} (89%) create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/util/ParserUtils.java create mode 100644 smack-tcp/src/main/java/org/jivesoftware/smack/tcp/sm/SMUtils.java create mode 100644 smack-tcp/src/main/java/org/jivesoftware/smack/tcp/sm/StreamManagementException.java create mode 100644 smack-tcp/src/main/java/org/jivesoftware/smack/tcp/sm/packet/StreamManagement.java create mode 100644 smack-tcp/src/main/java/org/jivesoftware/smack/tcp/sm/predicates/AfterXStanzas.java create mode 100644 smack-tcp/src/main/java/org/jivesoftware/smack/tcp/sm/predicates/ForEveryMessage.java create mode 100644 smack-tcp/src/main/java/org/jivesoftware/smack/tcp/sm/predicates/ForEveryStanza.java create mode 100644 smack-tcp/src/main/java/org/jivesoftware/smack/tcp/sm/predicates/ForMatchingPredicateOrAfterXStanzas.java create mode 100644 smack-tcp/src/main/java/org/jivesoftware/smack/tcp/sm/predicates/OnceForThisStanza.java create mode 100644 smack-tcp/src/main/java/org/jivesoftware/smack/tcp/sm/predicates/Predicate.java create mode 100644 smack-tcp/src/main/java/org/jivesoftware/smack/tcp/sm/predicates/ShortcutPredicates.java create mode 100644 smack-tcp/src/main/java/org/jivesoftware/smack/tcp/sm/provider/ParseStreamManagement.java create mode 100644 smack-tcp/src/test/java/org/jivesoftware/smack/tcp/sm/provider/ParseStreamManagementTest.java diff --git a/build.gradle b/build.gradle index b8840a72b..625f2526e 100644 --- a/build.gradle +++ b/build.gradle @@ -33,7 +33,7 @@ allprojects { // build, causing unnecessary rebuilds. builtDate = (new java.text.SimpleDateFormat("yyyy-MM-dd")).format(new Date()) oneLineDesc = 'An Open Source XMPP (Jabber) client library' - jxmppVersion = "0.2.0" + jxmppVersion = "0.3.0" } group = 'org.igniterealtime.smack' sourceCompatibility = 1.7 diff --git a/documentation/extensions/index.md b/documentation/extensions/index.md index a7514ebc4..f0816bf66 100644 --- a/documentation/extensions/index.md +++ b/documentation/extensions/index.md @@ -9,6 +9,14 @@ for many of the protocol extensions. This manual provides details about each of the "smackx" extensions, including what it is, how to use it, and some simple example code. +Currently supported XEPs of smack-tcp +------------------------------------- + +| Name | XEP | Description | +|---------------------------------------------|----------------------------------------------------------|----------------------------------------------------------------------------------------------------------| +| [Stream Management](streammanagement.html) | [XEP-0198](http://xmpp.org/extensions/xep-0198.html) | Allows active management of an XML Stream between two XMPP entities (stanza acknowledgement, stream resumption). | + + Smack Extensions and currently supported XEPs by Smack (smack-extensions) ------------------------------------------------------------------------- diff --git a/documentation/extensions/streammanagement.md b/documentation/extensions/streammanagement.md new file mode 100644 index 000000000..6f084f028 --- /dev/null +++ b/documentation/extensions/streammanagement.md @@ -0,0 +1,37 @@ +Stream Management +================= + +XMPPTCPConnection comes with support for Stream Management (SM). + +**XEP related:** [XEP-0198](http://xmpp.org/extensions/xep-0198.html) + +Known interoperability issues +----------------------------- + +- SM resumption failes on prosody when compression in sync flush mode is used with prosody. See [Prosody issue #433](https://code.google.com/p/lxmppd/issues/detail?id=433). + +Enabling stream management +------------------------ + +TODO + +Getting notifications about acknowledges stanzas +------------------------------------------------ + +TODO + +Requisting stanza acknowledgements from the server +-------------------------------------------------- + +### By using predicates + +TODO + +### Manually + +TODO + +Enable stream resumption +------------------------ + +TODO diff --git a/resources/getCopyright.sh b/resources/getCopyright.sh new file mode 100755 index 000000000..c34fd87da --- /dev/null +++ b/resources/getCopyright.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +SCRIPTDIR="$(dirname ${BASH_SOURCE[0]})" +BASEDIR=${SCRIPTDIR}/.. + +cd $BASEDIR +SUBPROJECTS=$(grep -oP "\'.*\'" settings.gradle | sed "s;';;g") +for p in $SUBPROJECTS; do + echo "Copyright notices for $p" + # Find all .java files in the project + find $p/src -type f -name "*.java" -print0 | \ + # Get the project string + xargs -0 grep -ohP '^.*\* Copyright \K.*' | \ + # Sort the output + sort | \ + # Remove duplicates + uniq | \ + # Split multi Copyright statemtents, e.g. "2001-2013 FooBar, 2014 Baz" + tr ',' '\n' | \ + # Remove whitespaces resulting from the previous split + sed "s/^[ \t]*//" | \ + # Remove dots at the end and '©' at the beginning + sed "s/^© //" | sed "s/\.$//" | sed "s/^(C) //" + echo -ne "\n" +done diff --git a/smack-bosh/src/main/java/org/jivesoftware/smack/bosh/BOSHPacketReader.java b/smack-bosh/src/main/java/org/jivesoftware/smack/bosh/BOSHPacketReader.java index 7293a07c9..7a9ef8914 100644 --- a/smack-bosh/src/main/java/org/jivesoftware/smack/bosh/BOSHPacketReader.java +++ b/smack-bosh/src/main/java/org/jivesoftware/smack/bosh/BOSHPacketReader.java @@ -20,9 +20,8 @@ package org.jivesoftware.smack.bosh; import java.io.StringReader; import org.jivesoftware.smack.packet.Packet; -import org.jivesoftware.smack.sasl.packet.SaslStanzas.Challenge; -import org.jivesoftware.smack.sasl.packet.SaslStanzas.SASLFailure; -import org.jivesoftware.smack.sasl.packet.SaslStanzas.Success; +import org.jivesoftware.smack.sasl.packet.SaslStreamElements.SASLFailure; +import org.jivesoftware.smack.sasl.packet.SaslStreamElements.Success; import org.jivesoftware.smack.util.PacketParserUtils; import org.jivesoftware.smack.XMPPException.StreamErrorException; import org.xmlpull.v1.XmlPullParserFactory; @@ -86,8 +85,6 @@ public class BOSHPacketReader implements BOSHClientResponseListener { final String challengeData = parser.nextText(); connection.getSASLAuthentication() .challengeReceived(challengeData); - connection.processPacket(new Challenge( - challengeData)); } else if (parser.getName().equals("success")) { connection.send(ComposableBody.builder() .setNamespaceDefinition("xmpp", XMPPBOSHConnection.XMPP_BOSH_NS) @@ -100,14 +97,12 @@ public class BOSHPacketReader implements BOSHClientResponseListener { .build()); Success success = new Success(parser.nextText()); connection.getSASLAuthentication().authenticated(success); - connection.processPacket(success); } else if (parser.getName().equals("features")) { parseFeatures(parser); } else if (parser.getName().equals("failure")) { if ("urn:ietf:params:xml:ns:xmpp-sasl".equals(parser.getNamespace(null))) { final SASLFailure failure = PacketParserUtils.parseSASLFailure(parser); connection.getSASLAuthentication().authenticationFailed(failure); - connection.processPacket(failure); } } else if (parser.getName().equals("error")) { throw new StreamErrorException(PacketParserUtils.parseStreamError(parser)); @@ -123,41 +118,7 @@ public class BOSHPacketReader implements BOSHClientResponseListener { } } - /** - * Parse and setup the XML stream features. - * - * @param parser the XML parser, positioned at the start of a message packet. - * @throws Exception if an exception occurs while parsing the packet. - */ private void parseFeatures(XmlPullParser parser) throws Exception { - boolean done = false; - while (!done) { - int eventType = parser.next(); - - if (eventType == XmlPullParser.START_TAG) { - if (parser.getName().equals("mechanisms")) { - // The server is reporting available SASL mechanisms. Store - // this information - // which will be used later while logging (i.e. - // authenticating) into - // the server - connection.getSASLAuthentication().setAvailableSASLMethods( - PacketParserUtils.parseMechanisms(parser)); - } else if (parser.getName().equals("bind")) { - // The server requires the client to bind a resource to the - // stream - connection.serverRequiresBinding(); - } else if (parser.getName().equals("session")) { - // The server supports sessions - connection.serverSupportsSession(); - } else if (parser.getName().equals("register")) { - connection.serverSupportsAccountCreation(); - } - } else if (eventType == XmlPullParser.END_TAG) { - if (parser.getName().equals("features")) { - done = true; - } - } - } + connection.parseFeatures0(parser); } } diff --git a/smack-bosh/src/main/java/org/jivesoftware/smack/bosh/XMPPBOSHConnection.java b/smack-bosh/src/main/java/org/jivesoftware/smack/bosh/XMPPBOSHConnection.java index 41f0ad168..ad3d4868e 100644 --- a/smack-bosh/src/main/java/org/jivesoftware/smack/bosh/XMPPBOSHConnection.java +++ b/smack-bosh/src/main/java/org/jivesoftware/smack/bosh/XMPPBOSHConnection.java @@ -38,9 +38,12 @@ import org.jivesoftware.smack.ConnectionCreationListener; import org.jivesoftware.smack.ConnectionListener; import org.jivesoftware.smack.Roster; import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.packet.Element; import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.packet.PlainStreamElement; import org.jivesoftware.smack.packet.Presence; import org.jivesoftware.smack.packet.Presence.Type; +import org.xmlpull.v1.XmlPullParser; import org.igniterealtime.jbosh.BOSHClient; import org.igniterealtime.jbosh.BOSHClientConfig; import org.igniterealtime.jbosh.BOSHClientConnEvent; @@ -85,7 +88,6 @@ public class XMPPBOSHConnection extends AbstractXMPPConnection { // Some flags which provides some info about the current state. private boolean connected = false; private boolean authenticated = false; - private boolean anonymous = false; private boolean isFirstInitialization = true; private boolean wasAuthenticated = false; private boolean done = false; @@ -104,11 +106,6 @@ public class XMPPBOSHConnection extends AbstractXMPPConnection { */ protected String sessionID = null; - /** - * The full JID of the authenticated user. - */ - private String user = null; - /** * Create a HTTP Binding connection to a XMPP server. * @@ -220,10 +217,6 @@ public class XMPPBOSHConnection extends AbstractXMPPConnection { return user; } - public boolean isAnonymous() { - return anonymous; - } - public boolean isAuthenticated() { return authenticated; } @@ -250,6 +243,10 @@ public class XMPPBOSHConnection extends AbstractXMPPConnection { if (authenticated) { throw new AlreadyLoggedInException(); } + + // Wait with SASL auth until the SASL mechanisms have been received + saslFeatureReceived.checkIfSuccessOrWaitOrThrow(); + // Do partial version of nameprep on the username. username = username.toLowerCase(Locale.US).trim(); @@ -264,41 +261,11 @@ public class XMPPBOSHConnection extends AbstractXMPPConnection { throw new SaslException("No non-anonymous SASL authentication mechanism available"); } - String response = bindResourceAndEstablishSession(resource); - // Set the user. - if (response != null) { - this.user = response; - // Update the serviceName with the one returned by the server - setServiceName(response); - } else { - this.user = username + "@" + getServiceName(); - if (resource != null) { - this.user += "/" + resource; - } - } + bindResourceAndEstablishSession(resource); - // Indicate that we're now authenticated. - authenticated = true; - anonymous = false; - - // Stores the autentication for future reconnection + // Stores the authentication for future reconnection setLoginInfo(username, password, resource); - - // If debugging is enabled, change the the debug window title to include - // the - // name we are now logged-in as.l - if (config.isDebuggerEnabled() && debugger != null) { - debugger.userHasLogged(user); - } - callConnectionAuthenticatedListener(); - - // Set presence to online. It is important that this is done after - // callConnectionAuthenticatedListener(), as this call will also - // eventually load the roster. And we should load the roster before we - // send the initial presence. - if (config.isSendPresence()) { - sendPacket(new Presence(Presence.Type.available)); - } + afterSuccessfulLogin(false, false); } public void loginAnonymously() throws XMPPException, SmackException, IOException { @@ -309,6 +276,9 @@ public class XMPPBOSHConnection extends AbstractXMPPConnection { throw new AlreadyLoggedInException(); } + // Wait with SASL auth until the SASL mechanisms have been received + saslFeatureReceived.checkIfSuccessOrWaitOrThrow(); + if (saslAuthentication.hasAnonymousAuthentication()) { saslAuthentication.authenticateAnonymously(); } @@ -317,38 +287,27 @@ public class XMPPBOSHConnection extends AbstractXMPPConnection { throw new SaslException("No anonymous SASL authentication mechanism available"); } - String response = bindResourceAndEstablishSession(null); - // Set the user value. - this.user = response; - // Update the serviceName with the one returned by the server - setServiceName(response); + bindResourceAndEstablishSession(null); - // Set presence to online. - if (config.isSendPresence()) { - sendPacket(new Presence(Presence.Type.available)); + afterSuccessfulLogin(true, false); + } + + @Override + public void send(PlainStreamElement element) throws NotConnectedException { + if (done) { + throw new NotConnectedException(); } - - // Indicate that we're now authenticated. - authenticated = true; - anonymous = true; - - // If debugging is enabled, change the the debug window title to include the - // name we are now logged-in as. - // If DEBUG_ENABLED was set to true AFTER the connection was created the debugger - // will be null - if (config.isDebuggerEnabled() && debugger != null) { - debugger.userHasLogged(user); - } - callConnectionAuthenticatedListener(); + sendElement(element); } @Override protected void sendPacketInternal(Packet packet) throws NotConnectedException { - if (done) { - throw new NotConnectedException(); - } + sendElement(packet); + } + + private void sendElement(Element element) { try { - send(ComposableBody.builder().setPayloadXML(packet.toXML().toString()).build()); + send(ComposableBody.builder().setPayloadXML(element.toXML().toString()).build()); } catch (BOSHException e) { LOGGER.log(Level.SEVERE, "BOSHException in sendPacketInternal", e); @@ -524,19 +483,8 @@ public class XMPPBOSHConnection extends AbstractXMPPConnection { return super.getSASLAuthentication(); } - @Override - protected void serverRequiresBinding() { - super.serverRequiresBinding(); - } - - @Override - protected void serverSupportsSession() { - super.serverSupportsSession(); - } - - @Override - protected void serverSupportsAccountCreation() { - super.serverSupportsAccountCreation(); + void parseFeatures0(XmlPullParser parser) throws Exception { + parseFeatures(parser); } /** diff --git a/smack-core/src/main/java/org/jivesoftware/smack/AbstractXMPPConnection.java b/smack-core/src/main/java/org/jivesoftware/smack/AbstractXMPPConnection.java index 8dc187cc7..8a076edfc 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/AbstractXMPPConnection.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/AbstractXMPPConnection.java @@ -20,6 +20,7 @@ import java.io.IOException; import java.io.Reader; import java.io.Writer; import java.util.Collection; +import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; @@ -28,27 +29,43 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.Logger; +import org.jivesoftware.smack.ConnectionConfiguration.SecurityMode; import org.jivesoftware.smack.SmackException.NoResponseException; import org.jivesoftware.smack.SmackException.NotConnectedException; import org.jivesoftware.smack.SmackException.ConnectionException; import org.jivesoftware.smack.SmackException.ResourceBindingNotOfferedException; +import org.jivesoftware.smack.SmackException.SecurityRequiredException; import org.jivesoftware.smack.XMPPException.XMPPErrorException; +import org.jivesoftware.smack.compress.packet.Compress; import org.jivesoftware.smack.compression.XMPPInputOutputStream; import org.jivesoftware.smack.debugger.SmackDebugger; import org.jivesoftware.smack.filter.IQReplyFilter; import org.jivesoftware.smack.filter.PacketFilter; import org.jivesoftware.smack.filter.PacketIDFilter; import org.jivesoftware.smack.packet.Bind; +import org.jivesoftware.smack.packet.CapsExtension; import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.Mechanisms; import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.packet.PacketExtension; import org.jivesoftware.smack.packet.Presence; +import org.jivesoftware.smack.packet.Registration; +import org.jivesoftware.smack.packet.RosterVer; import org.jivesoftware.smack.packet.Session; +import org.jivesoftware.smack.packet.StartTls; +import org.jivesoftware.smack.packet.PlainStreamElement; import org.jivesoftware.smack.rosterstore.RosterStore; +import org.jivesoftware.smack.util.PacketParserUtils; +import org.jxmpp.util.XmppStringUtils; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + public abstract class AbstractXMPPConnection implements XMPPConnection { private static final Logger LOGGER = Logger.getLogger(AbstractXMPPConnection.class.getName()); @@ -105,6 +122,15 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { protected final Map interceptors = new ConcurrentHashMap(); + protected final Lock connectionLock = new ReentrantLock(); + + protected final Map streamFeatures = new HashMap(); + + /** + * The full JID of the authenticated user. + */ + protected String user; + /** * */ @@ -125,7 +151,21 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { */ protected Writer writer; + /** + * Set to success if the last features stanza from the server has been parsed. A XMPP connection + * handshake can invoke multiple features stanzas, e.g. when TLS is activated a second feature + * stanza is send by the server. This is set to true once the last feature stanza has been + * parsed. + */ + protected final SynchronizationPoint lastFeaturesReceived = new SynchronizationPoint( + AbstractXMPPConnection.this); + /** + * Set to success if the sasl feature has been received. + */ + protected final SynchronizationPoint saslFeatureReceived = new SynchronizationPoint( + AbstractXMPPConnection.this); + /** * The SASLAuthentication manager that is responsible for authenticating with the server. */ @@ -142,21 +182,11 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { */ protected final ConnectionConfiguration config; - /** - * Holds the Caps Node information for the used XMPP service (i.e. the XMPP server) - */ - private String serviceCapsNode; - /** * Defines how the from attribute of outgoing stanzas should be handled. */ private FromMode fromMode = FromMode.OMITTED; - /** - * Stores whether the server supports rosterVersioning - */ - private boolean rosterVersioningSupported = false; - protected XMPPInputOutputStream compressionHandler; /** @@ -200,22 +230,6 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { */ protected int port; - /** - * Set to true if the server requires the connection to be binded in order to continue. - *

- * Note that we use AtomicBoolean here because it allows us to set the Boolean *object*, which - * we also use as synchronization object. A plain non-atomic Boolean object would be newly created - * for every change of the boolean value, which makes it useless as object for wait()/notify(). - */ - private AtomicBoolean bindingRequired = new AtomicBoolean(false); - - private boolean sessionSupported; - - /** - * - */ - private Exception connectionException; - /** * Flag that indicates if the user is currently authenticated with the server. */ @@ -227,6 +241,8 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { */ protected boolean wasAuthenticated = false; + private boolean anonymous = false; + /** * Create a new XMPPConnection to a XMPP server. * @@ -267,14 +283,14 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { @Override public abstract boolean isAuthenticated(); - @Override - public abstract boolean isAnonymous(); - @Override public abstract boolean isSecureConnection(); protected abstract void sendPacketInternal(Packet packet) throws NotConnectedException; + @Override + public abstract void send(PlainStreamElement element) throws NotConnectedException; + @Override public abstract boolean isUsingCompression(); @@ -292,22 +308,16 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { */ public void connect() throws SmackException, IOException, XMPPException { saslAuthentication.init(); - bindingRequired.set(false); - sessionSupported = false; - connectionException = null; + saslFeatureReceived.init(); + lastFeaturesReceived.init(); connectInternal(); } /** * Abstract method that concrete subclasses of XMPPConnection need to implement to perform their - * way of XMPP connection establishment. Implementations must guarantee that this method will - * block until the last features stanzas has been parsed and the features have been reported - * back to XMPPConnection (e.g. by calling @{link {@link AbstractXMPPConnection#serverRequiresBinding()} - * and such). - *

- * Also implementations are required to perform an automatic login if the previous connection - * state was logged (authenticated). - * + * way of XMPP connection establishment. Implementations are required to perform an automatic + * login if the previous connection state was logged (authenticated). + * * @throws SmackException * @throws IOException * @throws XMPPException @@ -383,44 +393,20 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { */ public abstract void loginAnonymously() throws XMPPException, SmackException, IOException; - /** - * Notification message saying that the server requires the client to bind a - * resource to the stream. - */ - protected void serverRequiresBinding() { - synchronized (bindingRequired) { - bindingRequired.set(true); - bindingRequired.notify(); - } - } + protected void bindResourceAndEstablishSession(String resource) throws XMPPErrorException, + IOException, SmackException { - /** - * Notification message saying that the server supports sessions. When a server supports - * sessions the client needs to send a Session packet after successfully binding a resource - * for the session. - */ - protected void serverSupportsSession() { - sessionSupported = true; - } + // Wait until either: + // - the servers last features stanza has been parsed + // - the timeout occurs + LOGGER.finer("Waiting for last features to be received before continuing with resource binding"); + lastFeaturesReceived.checkIfSuccessOrWait(); - protected String bindResourceAndEstablishSession(String resource) throws XMPPErrorException, - ResourceBindingNotOfferedException, NoResponseException, NotConnectedException { - synchronized (bindingRequired) { - if (!bindingRequired.get()) { - try { - bindingRequired.wait(getPacketReplyTimeout()); - } - catch (InterruptedException e) { - // Ignore - } - if (!bindingRequired.get()) { - // Server never offered resource binding, which is REQURIED in XMPP client and - // server - // implementations as per RFC6120 7.2 - throw new ResourceBindingNotOfferedException(); - } - } + if (!hasFeature(Bind.ELEMENT, Bind.NAMESPACE)) { + // Server never offered resource binding, which is REQURIED in XMPP client and + // server implementations as per RFC6120 7.2 + throw new ResourceBindingNotOfferedException(); } // Resource binding, see RFC6120 7. @@ -435,9 +421,10 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { throw e; } Bind response = packetCollector.nextResultOrThrow(); - String userJID = response.getJid(); + user = response.getJid(); + setServiceName(XmppStringUtils.parseDomain(user)); - if (sessionSupported && !getConfiguration().isLegacySessionDisabled()) { + if (hasFeature(Session.ELEMENT, Session.NAMESPACE) && !getConfiguration().isLegacySessionDisabled()) { Session session = new Session(); packetCollector = createPacketCollector(new PacketIDFilter(session)); try { @@ -448,58 +435,59 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { } packetCollector.nextResultOrThrow(); } - return userJID; } - protected void setConnectionException(Exception e) { - connectionException = e; - } + protected void afterSuccessfulLogin(final boolean anonymous, final boolean resumed) throws NotConnectedException { + // Indicate that we're now authenticated. + this.authenticated = true; + this.anonymous = anonymous; - protected void throwConnectionExceptionOrNoResponse() throws IOException, NoResponseException, SmackException { - if (connectionException != null) { - if (connectionException instanceof IOException) { - throw (IOException) connectionException; - } else if (connectionException instanceof SmackException) { - throw (SmackException) connectionException; - } else { - throw new SmackException(connectionException); - } - } else { - throw new NoResponseException(); + // If debugging is enabled, change the the debug window title to include the + // name we are now logged-in as. + // If DEBUG_ENABLED was set to true AFTER the connection was created the debugger + // will be null + if (config.isDebuggerEnabled() && debugger != null) { + debugger.userHasLogged(user); + } + callConnectionAuthenticatedListener(); + + // Set presence to online. It is important that this is done after + // callConnectionAuthenticatedListener(), as this call will also + // eventually load the roster. And we should load the roster before we + // send the initial presence. + if (config.isSendPresence() && !resumed) { + sendPacket(new Presence(Presence.Type.available)); } } - protected Reader getReader() { - return reader; - } - - protected Writer getWriter() { - return writer; + @Override + public boolean isAnonymous() { + return anonymous; } protected void setServiceName(String serviceName) { config.setServiceName(serviceName); } - + protected void setLoginInfo(String username, String password, String resource) { config.setLoginInfo(username, password, resource); } - - protected void serverSupportsAccountCreation() { - AccountManager.getInstance(this).setSupportsAccountCreation(true); - } protected void maybeResolveDns() throws Exception { config.maybeResolveDns(); } + protected Lock getConnectionLock() { + return connectionLock; + } + @Override public void sendPacket(Packet packet) throws NotConnectedException { if (!isConnected()) { throw new NotConnectedException(); } if (packet == null) { - throw new NullPointerException("Packet is null."); + throw new IllegalArgumentException("Packet must not be null"); } switch (fromMode) { case OMITTED: @@ -800,35 +788,6 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { } } - /** - * Set the servers Entity Caps node - * - * XMPPConnection holds this information in order to avoid a dependency to - * smack-extensions where EntityCapsManager lives from smack. - * - * @param node - */ - protected void setServiceCapsNode(String node) { - serviceCapsNode = node; - } - - @Override - public String getServiceCapsNode() { - return serviceCapsNode; - } - - @Override - public boolean isRosterVersioningSupported() { - return rosterVersioningSupported; - } - - /** - * Indicates that the server supports roster versioning as defined in XEP-0237. - */ - protected void setRosterVersioningSupported() { - rosterVersioningSupported = true; - } - @Override public long getPacketReplyTimeout() { return packetReplyTimeout; @@ -1055,6 +1014,112 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { return config.isRosterLoadedAtLogin(); } + protected final void parseFeatures(XmlPullParser parser) throws XmlPullParserException, + IOException, SecurityRequiredException, NotConnectedException { + streamFeatures.clear(); + final int initialDepth = parser.getDepth(); + while (true) { + int eventType = parser.next(); + + if (eventType == XmlPullParser.START_TAG && parser.getDepth() == initialDepth + 1) { + String name = parser.getName(); + String namespace = parser.getNamespace(); + switch (name) { + case StartTls.ELEMENT: + StartTls startTls = PacketParserUtils.parseStartTlsFeature(parser); + addStreamFeature(startTls); + break; + case Mechanisms.ELEMENT: + Mechanisms mechanisms = new Mechanisms(PacketParserUtils.parseMechanisms(parser)); + addStreamFeature(mechanisms); + break; + case Bind.ELEMENT: + addStreamFeature(Bind.Feature.INSTANCE); + break; + case CapsExtension.ELEMENT: + // Set the entity caps node for the server if one is send + // See http://xmpp.org/extensions/xep-0115.html#stream + String node = parser.getAttributeValue(null, "node"); + String ver = parser.getAttributeValue(null, "ver"); + String hash = parser.getAttributeValue(null, "hash"); + CapsExtension capsExtension = new CapsExtension(node, ver, hash); + addStreamFeature(capsExtension); + break; + case Session.ELEMENT: + addStreamFeature(Session.Feature.INSTANCE); + break; + case RosterVer.ELEMENT: + if(namespace.equals(RosterVer.NAMESPACE)) { + addStreamFeature(RosterVer.INSTANCE); + } + else { + LOGGER.severe("Unkown Roster Versioning Namespace: " + + namespace + + ". Roster versioning not enabled"); + } + break; + case Compress.Feature.ELEMENT: + Compress.Feature compression = PacketParserUtils.parseCompressionFeature(parser); + addStreamFeature(compression); + break; + case Registration.Feature.ELEMENT: + addStreamFeature(Registration.Feature.INSTANCE); + break; + default: + parseFeaturesSubclass(name, namespace, parser); + break; + } + } + else if (eventType == XmlPullParser.END_TAG && parser.getDepth() == initialDepth) { + break; + } + } + + if (hasFeature(Mechanisms.ELEMENT, Mechanisms.NAMESPACE)) { + // Only proceed with SASL auth if TLS is disabled or if the server doesn't announce it + if (!hasFeature(StartTls.ELEMENT, StartTls.NAMESPACE) + || config.getSecurityMode() == SecurityMode.disabled) { + saslFeatureReceived.reportSuccess(); + } + } + + // If the server reported the bind feature then we are that that we did SASL and maybe + // STARTTLS. We can then report that the last 'stream:features' have been parsed + if (hasFeature(Bind.ELEMENT, Bind.NAMESPACE)) { + if (!hasFeature(Compress.Feature.ELEMENT, Compress.NAMESPACE) + || !config.isCompressionEnabled()) { + // This was was last features from the server is either it did not contain + // compression or if we disabled it + lastFeaturesReceived.reportSuccess(); + } + } + afterFeaturesReceived(); + } + + protected void parseFeaturesSubclass (String name, String namespace, XmlPullParser parser) { + // Default implementation does nothing + } + + protected void afterFeaturesReceived() throws SecurityRequiredException, NotConnectedException { + // Default implementation does nothing + } + + @SuppressWarnings("unchecked") + @Override + public F getFeature(String element, String namespace) { + return (F) streamFeatures.get(XmppStringUtils.generateKey(element, namespace)); + } + + @Override + public boolean hasFeature(String element, String namespace) { + return getFeature(element, namespace) != null; + } + + protected void addStreamFeature(PacketExtension feature) { + String key = XmppStringUtils.generateKey(feature.getElementName(), feature.getNamespace()); + streamFeatures.put(key, feature); + } + private final ScheduledExecutorService removeCallbacksService = new ScheduledThreadPoolExecutor(1, new SmackExecutorThreadFactory(connectionCounterValue)); diff --git a/smack-core/src/main/java/org/jivesoftware/smack/Roster.java b/smack-core/src/main/java/org/jivesoftware/smack/Roster.java index 8408e828a..1df1238db 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/Roster.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/Roster.java @@ -37,7 +37,6 @@ import org.jivesoftware.smack.SmackException.NotConnectedException; import org.jivesoftware.smack.SmackException.NotLoggedInException; import org.jivesoftware.smack.XMPPException.XMPPErrorException; import org.jivesoftware.smack.filter.AndFilter; -import org.jivesoftware.smack.filter.IQReplyFilter; import org.jivesoftware.smack.filter.IQTypeFilter; import org.jivesoftware.smack.filter.PacketFilter; import org.jivesoftware.smack.filter.PacketTypeFilter; @@ -45,6 +44,7 @@ import org.jivesoftware.smack.packet.IQ; import org.jivesoftware.smack.packet.Packet; import org.jivesoftware.smack.packet.Presence; import org.jivesoftware.smack.packet.RosterPacket; +import org.jivesoftware.smack.packet.RosterVer; import org.jivesoftware.smack.packet.RosterPacket.Item; import org.jivesoftware.smack.rosterstore.RosterStore; import org.jxmpp.util.XmppStringUtils; @@ -227,12 +227,15 @@ public class Roster { } RosterPacket packet = new RosterPacket(); - if (rosterStore != null && connection.isRosterVersioningSupported()) { + if (rosterStore != null && isRosterVersioningSupported()) { packet.setVersion(rosterStore.getRosterVersion()); } - PacketFilter filter = new IQReplyFilter(packet, connection); - connection.addPacketListener(new RosterResultListener(), filter); - connection.sendPacket(packet); + connection.sendIqWithResponseCallback(packet, new RosterResultListener(), new ExceptionCallback() { + @Override + public void processException(Exception exception) { + LOGGER.log(Level.SEVERE, "Exception reloading roster" , exception); + } + }); } /** @@ -791,6 +794,10 @@ public class Roster { || item.getItemType().equals(RosterPacket.ItemType.both); } + private boolean isRosterVersioningSupported() { + return connection.hasFeature(RosterVer.ELEMENT, RosterVer.NAMESPACE); + } + /** * An enumeration for the subscription mode options. */ @@ -939,23 +946,13 @@ public class Roster { } /** - * Handles the case of the empty IQ-result for roster versioning. - * - * Intended to listen for a concrete roster result and deregisters - * itself after a processed packet. + * Handles roster reults as described in RFC 6121 2.1.4 */ private class RosterResultListener implements PacketListener { @Override public void processPacket(Packet packet) { - connection.removePacketListener(this); - - IQ result = (IQ)packet; - if (!result.getType().equals(IQ.Type.result)) { - LOGGER.severe("Roster result IQ not of type result. Packet: " + result.toXML()); - return; - } - + LOGGER.fine("RosterResultListener received stanza"); Collection addedEntries = new ArrayList(); Collection updatedEntries = new ArrayList(); Collection deletedEntries = new ArrayList(); diff --git a/smack-core/src/main/java/org/jivesoftware/smack/SASLAuthentication.java b/smack-core/src/main/java/org/jivesoftware/smack/SASLAuthentication.java index 70e7d2085..14da9a1e9 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/SASLAuthentication.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/SASLAuthentication.java @@ -19,17 +19,17 @@ package org.jivesoftware.smack; import org.jivesoftware.smack.SmackException.NoResponseException; import org.jivesoftware.smack.XMPPException.XMPPErrorException; +import org.jivesoftware.smack.packet.Mechanisms; import org.jivesoftware.smack.sasl.SASLAnonymous; import org.jivesoftware.smack.sasl.SASLErrorException; import org.jivesoftware.smack.sasl.SASLMechanism; -import org.jivesoftware.smack.sasl.packet.SaslStanzas.SASLFailure; -import org.jivesoftware.smack.sasl.packet.SaslStanzas.Success; +import org.jivesoftware.smack.sasl.packet.SaslStreamElements.SASLFailure; +import org.jivesoftware.smack.sasl.packet.SaslStreamElements.Success; import javax.security.auth.callback.CallbackHandler; import java.io.IOException; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -128,13 +128,12 @@ public class SASLAuthentication { } private final AbstractXMPPConnection connection; - private Collection serverMechanisms = new ArrayList(); private SASLMechanism currentMechanism = null; /** * Boolean indicating if SASL negotiation has finished and was successful. */ - private boolean saslNegotiated; + private boolean authenticationSuccessful; /** * Either of type {@link SmackException} or {@link SASLErrorException} @@ -152,7 +151,7 @@ public class SASLAuthentication { * @return true if the server offered ANONYMOUS SASL as a way to authenticate users. */ public boolean hasAnonymousAuthentication() { - return serverMechanisms.contains("ANONYMOUS"); + return serverMechanisms().contains("ANONYMOUS"); } /** @@ -161,7 +160,7 @@ public class SASLAuthentication { * @return true if the server offered SASL authentication besides ANONYMOUS SASL. */ public boolean hasNonAnonymousAuthentication() { - return !serverMechanisms.isEmpty() && (serverMechanisms.size() != 1 || !hasAnonymousAuthentication()); + return !serverMechanisms().isEmpty() && (serverMechanisms().size() != 1 || !hasAnonymousAuthentication()); } /** @@ -197,7 +196,7 @@ public class SASLAuthentication { maybeThrowException(); - if (!saslNegotiated) { + if (!authenticationSuccessful) { throw new NoResponseException(); } } @@ -244,7 +243,7 @@ public class SASLAuthentication { maybeThrowException(); - if (!saslNegotiated) { + if (!authenticationSuccessful) { throw new NoResponseException(); } } @@ -283,7 +282,7 @@ public class SASLAuthentication { maybeThrowException(); - if (!saslNegotiated) { + if (!authenticationSuccessful) { throw new NoResponseException(); } } @@ -299,17 +298,6 @@ public class SASLAuthentication { } } } - /** - * Sets the available SASL mechanism reported by the server. The server will report the - * available SASL mechanism once the TLS negotiation was successful. This information is - * stored and will be used when doing the authentication for logging in the user. - * - * @param mechanisms collection of strings with the available SASL mechanism reported - * by the server. - */ - public void setAvailableSASLMethods(Collection mechanisms) { - this.serverMechanisms = mechanisms; - } /** * Wrapper for {@link #challengeReceived(String, boolean)}, with finalChallenge set @@ -355,7 +343,7 @@ public class SASLAuthentication { if (success.getData() != null) { challengeReceived(success.getData(), true); } - saslNegotiated = true; + authenticationSuccessful = true; // Wake up the thread that is waiting in the #authenticate method synchronized (this) { notify(); @@ -381,13 +369,17 @@ public class SASLAuthentication { } } + public boolean authenticationSuccessful() { + return authenticationSuccessful; + } + /** * Initializes the internal state in order to be able to be reused. The authentication * is used by the connection at the first login and then reused after the connection * is disconnected and then reconnected. */ protected void init() { - saslNegotiated = false; + authenticationSuccessful = false; saslException = null; } @@ -404,7 +396,7 @@ public class SASLAuthentication { continue; } } - if (serverMechanisms.contains(mechanismName)) { + if (serverMechanisms().contains(mechanismName)) { // Create a new instance of the SASLMechanism for every authentication attempt. selectedMechanism = mechanism.instanceForAuthentication(connection); break; @@ -412,4 +404,9 @@ public class SASLAuthentication { } return selectedMechanism; } + + private List serverMechanisms() { + Mechanisms mechanisms = connection.getFeature(Mechanisms.ELEMENT, Mechanisms.NAMESPACE); + return mechanisms.getMechanisms(); + } } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/SmackException.java b/smack-core/src/main/java/org/jivesoftware/smack/SmackException.java index d81a0cc15..d9ef9881a 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/SmackException.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/SmackException.java @@ -89,6 +89,17 @@ public class SmackException extends Exception { } } + public static class AlreadyConnectedException extends SmackException { + + /** + * + */ + private static final long serialVersionUID = 5011416918049135231L; + + public AlreadyConnectedException() { + } + } + public static class NotConnectedException extends SmackException { /** @@ -120,6 +131,10 @@ public class SmackException extends Exception { public SecurityRequiredException() { } + + public SecurityRequiredException(String message) { + super(message); + } } public static class SecurityNotPossibleException extends SmackException { diff --git a/smack-core/src/main/java/org/jivesoftware/smack/SynchronizationPoint.java b/smack-core/src/main/java/org/jivesoftware/smack/SynchronizationPoint.java new file mode 100644 index 000000000..4037fb64d --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/SynchronizationPoint.java @@ -0,0 +1,190 @@ +/** + * + * Copyright © 2014 Florian Schmaus + * + * 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; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.jivesoftware.smack.SmackException.NoResponseException; +import org.jivesoftware.smack.SmackException.NotConnectedException; +import org.jivesoftware.smack.packet.TopLevelStreamElement; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.packet.PlainStreamElement; + +public class SynchronizationPoint { + + private static final Logger LOGGER = Logger.getLogger(SynchronizationPoint.class.getName()); + + private final AbstractXMPPConnection connection; + private final Lock connectionLock; + private final Condition condition; + + private State state; + private E failureException; + + public SynchronizationPoint(AbstractXMPPConnection connection) { + this.connection = connection; + this.connectionLock = connection.getConnectionLock(); + this.condition = connection.getConnectionLock().newCondition(); + init(); + } + + public void init() { + state = State.Initial; + failureException = null; + } + + public void sendAndWaitForResponse(TopLevelStreamElement request) throws NoResponseException, + NotConnectedException { + assert (state == State.Initial); + connectionLock.lock(); + try { + if (request != null) { + if (request instanceof Packet) { + connection.sendPacket((Packet) request); + } + else if (request instanceof PlainStreamElement){ + connection.send((PlainStreamElement) request); + } else { + throw new IllegalStateException("Unsupported element type"); + } + state = State.RequestSent; + } + waitForConditionOrTimeout(); + } + finally { + connectionLock.unlock(); + } + checkForResponse(); + } + + public void sendAndWaitForResponseOrThrow(PlainStreamElement request) throws E, NoResponseException, + NotConnectedException { + sendAndWaitForResponse(request); + switch (state) { + case Failure: + if (failureException != null) { + throw failureException; + } + break; + default: + // Success, do nothing + } + } + + public void checkIfSuccessOrWaitOrThrow() throws NoResponseException, E { + checkIfSuccessOrWait(); + if (state == State.Failure) { + throw failureException; + } + } + + public void checkIfSuccessOrWait() throws NoResponseException { + connectionLock.lock(); + try { + if (state == State.Success) { + // Return immediately + return; + } + waitForConditionOrTimeout(); + } finally { + connectionLock.unlock(); + } + checkForResponse(); + } + + public void reportSuccess() { + connectionLock.lock(); + try { + state = State.Success; + condition.signal(); + } + finally { + connectionLock.unlock(); + } + } + + public void reportFailure() { + reportFailure(null); + } + + public void reportFailure(E failureException) { + connectionLock.lock(); + try { + state = State.Failure; + this.failureException = failureException; + condition.signal(); + } + finally { + connectionLock.unlock(); + } + } + + public boolean wasSuccessful() { + return state == State.Success; + } + + public boolean requestSent() { + return state == State.RequestSent; + } + + private void waitForConditionOrTimeout() { + long remainingWait = TimeUnit.MILLISECONDS.toNanos(connection.getPacketReplyTimeout()); + while (state == State.RequestSent || state == State.Initial) { + try { + remainingWait = condition.awaitNanos( + remainingWait); + if (remainingWait <= 0) { + state = State.NoResponse; + break; + } + } catch (InterruptedException e) { + LOGGER.log(Level.FINE, "was interrupted while waiting, this should not happen", e); + } + } + } + + /** + * Check for a response and throw a {@link NoResponseException} if there was none. + *

+ * The exception is thrown, if state is one of 'Initial', 'NoResponse' or 'RequestSent' + *

+ * @throws NoResponseException + */ + private void checkForResponse() throws NoResponseException { + switch (state) { + case Initial: + case NoResponse: + case RequestSent: + throw new NoResponseException(); + default: + // Do nothing + break; + } + } + + private enum State { + Initial, + RequestSent, + NoResponse, + Success, + Failure, + } +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/XMPPConnection.java b/smack-core/src/main/java/org/jivesoftware/smack/XMPPConnection.java index b81bd88a4..590756f20 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/XMPPConnection.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/XMPPConnection.java @@ -23,6 +23,8 @@ import org.jivesoftware.smack.filter.IQReplyFilter; import org.jivesoftware.smack.filter.PacketFilter; import org.jivesoftware.smack.packet.IQ; import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smack.packet.PlainStreamElement; import org.jivesoftware.smack.rosterstore.RosterStore; /** @@ -151,6 +153,13 @@ public interface XMPPConnection { */ public void sendPacket(Packet packet) throws NotConnectedException; + /** + * + * @param element + * @throws NotConnectedException + */ + public void send(PlainStreamElement element) throws NotConnectedException; + /** * Returns the roster for the user. *

@@ -272,23 +281,6 @@ public interface XMPPConnection { */ public void removePacketInterceptor(PacketInterceptor packetInterceptor); - /** - * Retrieve the servers Entity Caps node - * - * XMPPConnection holds this information in order to avoid a dependency to - * smackx where EntityCapsManager lives from smack. - * - * @return the servers entity caps node - */ - public String getServiceCapsNode(); - - /** - * Returns true if the server supports roster versioning as defined in XEP-0237. - * - * @return true if the server supports roster versioning - */ - public boolean isRosterVersioningSupported(); - /** * Returns the current value of the reply timeout in milliseconds for request for this * XMPPConnection instance. @@ -354,13 +346,33 @@ public interface XMPPConnection { /** * Returns true if the roster will be loaded from the server when logging in. This - * is the common behaviour for clients but sometimes clients may want to differ this + * is the common behavior for clients but sometimes clients may want to differ this * or just never do it if not interested in rosters. * * @return true if the roster will be loaded from the server when logging in. + * @see RFC 6121 2.2 - Retrieving the Roster on Login */ public boolean isRosterLoadedAtLogin(); + /** + * Get the feature packet extensions for a given stream feature of the + * server, or null if the server doesn't support that feature. + * + * @param element + * @param namespace + * @return a packet extensions of the feature or null + */ + public F getFeature(String element, String namespace); + + /** + * Return true if the server supports the given stream feature. + * + * @param element + * @param namespace + * @return + */ + public boolean hasFeature(String element, String namespace); + /** * Send a stanza and wait asynchronously for a response by using replyFilter. *

@@ -464,4 +476,5 @@ public interface XMPPConnection { * @return the timestamp in milliseconds */ public long getLastStanzaReceived(); + } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/XMPPException.java b/smack-core/src/main/java/org/jivesoftware/smack/XMPPException.java index a27819f02..4dd802097 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/XMPPException.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/XMPPException.java @@ -55,7 +55,6 @@ public abstract class XMPPException extends Exception { super(message); } - /** * Creates a new XMPPException with a description of the exception and the * Throwable that was the root cause of the exception. diff --git a/smack-core/src/main/java/org/jivesoftware/smack/compress/packet/Compress.java b/smack-core/src/main/java/org/jivesoftware/smack/compress/packet/Compress.java new file mode 100644 index 000000000..bb2ccd68c --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/compress/packet/Compress.java @@ -0,0 +1,90 @@ +/** + * + * Copyright © 2014 Florian Schmaus + * + * 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.compress.packet; + +import java.util.Collections; +import java.util.List; + +import org.jivesoftware.smack.packet.FullStreamElement; +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smack.util.XmlStringBuilder; + +public class Compress extends FullStreamElement { + + public static final String ELEMENT = "compress"; + public static final String NAMESPACE = "http://jabber.org/protocol/compress"; + + public final String method; + + public Compress(String method) { + this.method = method; + } + + @Override + public String getElementName() { + return ELEMENT; + } + + @Override + public String getNamespace() { + return NAMESPACE; + } + + @Override + public XmlStringBuilder toXML() { + XmlStringBuilder xml = new XmlStringBuilder(this); + xml.rightAngleBracket(); + xml.element("method", method); + xml.closeElement(this); + return xml; + } + + public static class Feature implements PacketExtension { + public static final String ELEMENT = "compression"; + + public final List methods; + + public Feature(List methods) { + this.methods = methods; + } + + public List getMethods() { + return Collections.unmodifiableList(methods); + } + + @Override + public String getNamespace() { + return NAMESPACE; + } + + @Override + public String getElementName() { + return ELEMENT; + } + + @Override + public XmlStringBuilder toXML() { + XmlStringBuilder xml = new XmlStringBuilder(this); + xml.rightAngleBracket(); + for (String method : methods) { + xml.element("method", method); + } + xml.closeElement(this); + return xml; + } + } +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/compress/packet/Compressed.java b/smack-core/src/main/java/org/jivesoftware/smack/compress/packet/Compressed.java new file mode 100644 index 000000000..29634f4cc --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/compress/packet/Compressed.java @@ -0,0 +1,45 @@ +/** + * + * Copyright © 2014 Florian Schmaus + * + * 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.compress.packet; + +import org.jivesoftware.smack.packet.FullStreamElement; + +public class Compressed extends FullStreamElement { + + public static final String ELEMENT = "compressed"; + public static final String NAMESPACE = Compress.NAMESPACE; + + public static final Compressed INSTANCE = new Compressed(); + + private Compressed() { + } + + @Override + public String getElementName() { + return ELEMENT; + } + + @Override + public String getNamespace() { + return NAMESPACE; + } + + @Override + public String toXML() { + return '<' + ELEMENT + " xmlns='" + NAMESPACE + "'/>"; + } +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/compression/Java7ZlibInputOutputStream.java b/smack-core/src/main/java/org/jivesoftware/smack/compression/Java7ZlibInputOutputStream.java index 0f7933faa..873f8b599 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/compression/Java7ZlibInputOutputStream.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/compression/Java7ZlibInputOutputStream.java @@ -110,6 +110,7 @@ public class Java7ZlibInputOutputStream extends XMPPInputOutputStream { flushMethodInt = FULL_FLUSH_INT; } return new DeflaterOutputStream(outputStream, new Deflater(compressionLevel)) { + @Override public void flush() throws IOException { if (!supported) { super.flush(); diff --git a/smack-core/src/main/java/org/jivesoftware/smack/debugger/ConsoleDebugger.java b/smack-core/src/main/java/org/jivesoftware/smack/debugger/ConsoleDebugger.java index 8c13b11b1..79cfe5240 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/debugger/ConsoleDebugger.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/debugger/ConsoleDebugger.java @@ -176,10 +176,11 @@ public class ConsoleDebugger implements SmackDebugger { } public void userHasLogged(String user) { - boolean isAnonymous = "".equals(XmppStringUtils.parseLocalpart(user)); + String localpart = XmppStringUtils.parseLocalpart(user); + boolean isAnonymous = "".equals(localpart); String title = "User logged (" + connection.hashCode() + "): " - + (isAnonymous ? "" : XmppStringUtils.parseBareAddress(user)) + + (isAnonymous ? "" : localpart) + "@" + connection.getServiceName() + ":" diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/Bind.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/Bind.java index 7a5bada19..ef5cc4232 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/packet/Bind.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/Bind.java @@ -51,18 +51,6 @@ public class Bind extends IQ { return jid; } - @Override - public XmlStringBuilder getChildElementXML() { - XmlStringBuilder xml = new XmlStringBuilder(); - - xml.halfOpenElement(ELEMENT).xmlnsAttribute(NAMESPACE).rightAngleBracket(); - xml.optElement("resource", resource); - xml.optElement("jid", jid); - xml.closeElement(ELEMENT); - - return xml; - } - public static Bind newSet(String resource) { Bind bind = new Bind(resource, null); bind.setType(IQ.Type.set); @@ -72,4 +60,38 @@ public class Bind extends IQ { public static Bind newResult(String jid) { return new Bind(null, jid); } + + @Override + public XmlStringBuilder getChildElementXML() { + XmlStringBuilder xml = new XmlStringBuilder(); + xml.halfOpenElement(ELEMENT).xmlnsAttribute(NAMESPACE).rightAngleBracket(); + xml.optElement("resource", resource); + xml.optElement("jid", jid); + xml.closeElement(ELEMENT); + return xml; + } + + public static class Feature implements PacketExtension { + + public static final Feature INSTANCE = new Feature(); + + private Feature() { + } + + @Override + public String getElementName() { + return ELEMENT; + } + + @Override + public String getNamespace() { + return NAMESPACE; + } + + @Override + public String toXML() { + return '<' + ELEMENT + " xmlns='" + NAMESPACE + "'/>"; + } + + } } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/caps/packet/CapsExtension.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/CapsExtension.java similarity index 63% rename from smack-extensions/src/main/java/org/jivesoftware/smackx/caps/packet/CapsExtension.java rename to smack-core/src/main/java/org/jivesoftware/smack/packet/CapsExtension.java index 6746e6a63..8ec70c71f 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/caps/packet/CapsExtension.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/CapsExtension.java @@ -14,13 +14,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.jivesoftware.smackx.caps.packet; +package org.jivesoftware.smack.packet; -import org.jivesoftware.smack.packet.PacketExtension; import org.jivesoftware.smack.util.XmlStringBuilder; -import org.jivesoftware.smackx.caps.EntityCapsManager; +/** + * A XEP-0115 Entity Capabilities extension. + *

+ * Note that this is currently in smack-core as it's a potential stream feature. + * TODO: In feature versions of Smack, it should be possible to register + * "providers" for stream features too, so that this class can be moved back to + * smack-extensions. + *

+ */ public class CapsExtension implements PacketExtension { + public static final String NAMESPACE = "http://jabber.org/protocol/caps"; + public static final String ELEMENT = "c"; private final String node, ver, hash; @@ -31,11 +40,11 @@ public class CapsExtension implements PacketExtension { } public String getElementName() { - return EntityCapsManager.ELEMENT; + return ELEMENT; } public String getNamespace() { - return EntityCapsManager.NAMESPACE; + return NAMESPACE; } public String getNode() { @@ -50,17 +59,24 @@ public class CapsExtension implements PacketExtension { return hash; } - /* + /** + *
      *  
+     *     hash='sha-1'
+     *     node='http://code.google.com/p/exodus'
+     *     ver='QgayPKawpkPSDYmwT/WM94uAlu0='/>
+     * 
* */ - public CharSequence toXML() { + @Override + public XmlStringBuilder toXML() { XmlStringBuilder xml = new XmlStringBuilder(this); xml.attribute("hash", hash).attribute("node", node).attribute("ver", ver); xml.closeEmptyElement(); return xml; } + + public static CapsExtension from(Packet stanza) { + return stanza.getExtension(ELEMENT, NAMESPACE); + } } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/Element.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/Element.java index 6e5292570..17b38280d 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/packet/Element.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/Element.java @@ -24,14 +24,7 @@ package org.jivesoftware.smack.packet; public interface Element { /** - * Returns the root element name. - * - * @return the element name. - */ - public String getElementName(); - - /** - * Returns the XML representation of the PacketExtension. + * Returns the XML representation of this Element. * * @return the packet extension as XML. */ diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/FullStreamElement.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/FullStreamElement.java new file mode 100644 index 000000000..8666f4d88 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/FullStreamElement.java @@ -0,0 +1,28 @@ +/** + * + * Copyright © 2014 Florian Schmaus + * + * 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.packet; + +/** + * Base class for Stream elements. Everything that is not a stanza (RFC 6120 8.), ie. message, + * presence and iq, should sublcass this class instead of {@link Packet}. + * + * @author Florian Schmaus + */ +public abstract class FullStreamElement extends PlainStreamElement implements PacketExtension { + +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/IQ.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/IQ.java index e31d18dc7..e2f941fc2 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/packet/IQ.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/IQ.java @@ -40,6 +40,7 @@ import org.jivesoftware.smack.util.XmlStringBuilder; */ public abstract class IQ extends Packet { + public static final String ELEMENT = "iq"; public static final String QUERY_ELEMENT = "query"; private Type type = Type.get; @@ -78,7 +79,7 @@ public abstract class IQ extends Packet { @Override public CharSequence toXML() { XmlStringBuilder buf = new XmlStringBuilder(); - buf.halfOpenElement("iq"); + buf.halfOpenElement(ELEMENT); addCommonAttributes(buf); if (type == null) { buf.attribute("type", "get"); @@ -94,7 +95,7 @@ public abstract class IQ extends Packet { if (error != null) { buf.append(error.toXML()); } - buf.closeElement("iq"); + buf.closeElement(ELEMENT); return buf; } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/Mechanisms.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/Mechanisms.java new file mode 100644 index 000000000..17657f7a7 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/Mechanisms.java @@ -0,0 +1,66 @@ +/** + * + * Copyright © 2014 Florian Schmaus + * + * 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.packet; + +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +import org.jivesoftware.smack.util.XmlStringBuilder; + +public class Mechanisms implements PacketExtension { + + public static final String ELEMENT = "mechanisms"; + public static final String NAMESPACE = "urn:ietf:params:xml:ns:xmpp-sasl"; + + public final List mechanisms = new LinkedList(); + + public Mechanisms(String mechanism) { + mechanisms.add(mechanism); + } + + public Mechanisms(Collection mechanisms) { + this.mechanisms.addAll(mechanisms); + } + + @Override + public String getElementName() { + return ELEMENT; + } + + @Override + public String getNamespace() { + return NAMESPACE; + } + + public List getMechanisms() { + return Collections.unmodifiableList(mechanisms); + } + + @Override + public XmlStringBuilder toXML() { + XmlStringBuilder xml = new XmlStringBuilder(this); + xml.rightAngleBracket(); + for (String mechanism : mechanisms) { + xml.element("mechanism", mechanism); + } + xml.closeElement(this); + return xml; + } + +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/Message.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/Message.java index d5f74c342..f8fcb0693 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/packet/Message.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/Message.java @@ -53,6 +53,7 @@ import org.jivesoftware.smack.util.XmlStringBuilder; */ public class Message extends XmlLangStanza { + public static final String ELEMENT = "message"; public static final String BODY = "body"; private Type type = Type.normal; @@ -394,7 +395,7 @@ public class Message extends XmlLangStanza { @Override public XmlStringBuilder toXML() { XmlStringBuilder buf = new XmlStringBuilder(); - buf.halfOpenElement("message"); + buf.halfOpenElement(ELEMENT); buf.xmllangAttribute(language); addCommonAttributes(buf); if (type != Type.normal) { @@ -440,7 +441,7 @@ public class Message extends XmlLangStanza { } // Add packet extensions, if any are defined. buf.append(getExtensionsXML()); - buf.closeElement("message"); + buf.closeElement(ELEMENT); return buf; } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/NamedElement.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/NamedElement.java new file mode 100644 index 000000000..6a1f52ba4 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/NamedElement.java @@ -0,0 +1,33 @@ +/** + * + * Copyright © 2014 Florian Schmaus + * + * 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.packet; + +/** + * Interface to represent a XML element. This is similar to {@link PacketExtension}, but does not + * carry a namespace and is usually included as child element of an packet extension. + */ +public interface NamedElement extends Element { + + /** + * Returns the root element name. + * + * @return the element name. + */ + public String getElementName(); + +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/Packet.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/Packet.java index 782f7be8a..e3dd49830 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/packet/Packet.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/Packet.java @@ -34,7 +34,7 @@ import java.util.concurrent.atomic.AtomicLong; * * @author Matt Tucker */ -public abstract class Packet { +public abstract class Packet extends TopLevelStreamElement { public static final String TEXT = "text"; public static final String ITEM = "item"; @@ -246,15 +246,6 @@ public abstract class Packet { packetExtensions.remove(extension); } - /** - * Returns the packet as XML. Every concrete extension of Packet must implement - * this method. In addition to writing out packet-specific data, every sub-class - * should also write out the error and the extensions data if they are defined. - * - * @return the XML format of the packet as a String. - */ - public abstract CharSequence toXML(); - /** * Returns the extension sub-packets (including properties data) as an XML * String, or the Empty String if there are no packet extensions. diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/PacketExtension.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/PacketExtension.java index a7827ae82..060262349 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/packet/PacketExtension.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/PacketExtension.java @@ -28,7 +28,7 @@ package org.jivesoftware.smack.packet; * @see org.jivesoftware.smack.provider.PacketExtensionProvider * @author Matt Tucker */ -public interface PacketExtension extends Element { +public interface PacketExtension extends NamedElement { /** * Returns the root element XML namespace. diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/PlainStreamElement.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/PlainStreamElement.java new file mode 100644 index 000000000..fa2394c89 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/PlainStreamElement.java @@ -0,0 +1,34 @@ +/** + * + * Copyright © 2014 Florian Schmaus + * + * 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.packet; + +/** + * Plain stream elements, ie. everything that is not a stanza as defined + * RFC 6120 8. Stanzas are {@link Message}, {@link Presence} and {@link IQ}. + * Everything else should sublcass this class instead of {@link Packet}. + *

+ * It is important to cleanly distinguish between stanzas and non-stanzas. For + * example plain stream elements don't count into the stanza count of XEP-198 + * Stream Management. + *

+ * + * @author Florian Schmaus + */ +public abstract class PlainStreamElement extends TopLevelStreamElement { + +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/Registration.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/Registration.java index b4b7894dc..a52b8b620 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/packet/Registration.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/Registration.java @@ -46,6 +46,8 @@ import org.jivesoftware.smack.util.XmlStringBuilder; */ public class Registration extends IQ { + public static final String NAMESPACE = "jabber:iq:register"; + private String instructions = null; private Map attributes = null; @@ -90,8 +92,8 @@ public class Registration extends IQ { @Override public XmlStringBuilder getChildElementXML() { XmlStringBuilder xml = new XmlStringBuilder(); - xml.halfOpenElement("query"); - xml.xmlnsAttribute("jabber:iq:register"); + xml.halfOpenElement(QUERY_ELEMENT); + xml.xmlnsAttribute(NAMESPACE); xml.rightAngleBracket(); xml.optElement("instructions", instructions); if (attributes != null && attributes.size() > 0) { @@ -102,7 +104,33 @@ public class Registration extends IQ { } // Add packet extensions, if any are defined. xml.append(getExtensionsXML()); - xml.closeElement("query"); + xml.closeElement(QUERY_ELEMENT); return xml; } + + public static class Feature implements PacketExtension { + + public static final String ELEMENT = "register"; + public static final String NAMESPACE = "http://jabber.org/features/iq-register"; + public static final Feature INSTANCE = new Registration.Feature(); + + private Feature() { + } + + @Override + public String getElementName() { + return ELEMENT; + } + + @Override + public CharSequence toXML() { + return '<' + ELEMENT + " xmlns='" + NAMESPACE + "'/>"; + } + + @Override + public String getNamespace() { + return NAMESPACE; + } + + } } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/RosterVer.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/RosterVer.java new file mode 100644 index 000000000..850fe38f2 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/RosterVer.java @@ -0,0 +1,48 @@ +/** + * + * Copyright © 2014 Florian Schmaus + * + * 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.packet; + +import org.jivesoftware.smack.util.XmlStringBuilder; + +public class RosterVer implements PacketExtension { + + public static final String ELEMENT = "ver"; + public static final String NAMESPACE = "urn:xmpp:features:rosterver"; + + public static final RosterVer INSTANCE = new RosterVer(); + + private RosterVer() { + } + + @Override + public String getElementName() { + return ELEMENT; + } + + @Override + public String getNamespace() { + return NAMESPACE; + } + + @Override + public XmlStringBuilder toXML() { + XmlStringBuilder xml = new XmlStringBuilder(this); + xml.closeEmptyElement(); + return xml; + } + +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/Session.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/Session.java index cf67bef97..8f6ed5ea9 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/packet/Session.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/Session.java @@ -32,12 +32,40 @@ package org.jivesoftware.smack.packet; */ public class Session extends IQ { + public static final String ELEMENT = "session"; + public static final String NAMESPACE = "urn:ietf:params:xml:ns:xmpp-session"; + + private static final String SESSION = '<' + ELEMENT + " xmlns='" + NAMESPACE + "'/>"; + public Session() { setType(IQ.Type.set); } @Override - public CharSequence getChildElementXML() { - return ""; + public String getChildElementXML() { + return SESSION; + } + + public static class Feature implements PacketExtension { + + public static final Session.Feature INSTANCE = new Feature(); + + private Feature() { + } + + @Override + public String getElementName() { + return ELEMENT; + } + + @Override + public String getNamespace() { + return NAMESPACE; + } + + @Override + public String toXML() { + return SESSION; + } } } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/StartTls.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/StartTls.java new file mode 100644 index 000000000..a3398b57a --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/StartTls.java @@ -0,0 +1,59 @@ +/** + * + * Copyright © 2014 Florian Schmaus + * + * 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.packet; + +import org.jivesoftware.smack.util.XmlStringBuilder; + +public class StartTls extends FullStreamElement { + + public static final String ELEMENT = "starttls"; + public static final String NAMESPACE = "urn:ietf:params:xml:ns:xmpp-tls"; + + private final boolean required; + + public StartTls() { + this(false); + } + + public StartTls(boolean required) { + this.required = required; + } + + public boolean required() { + return required; + } + + @Override + public String getElementName() { + return ELEMENT; + } + + @Override + public String getNamespace() { + return NAMESPACE; + } + + @Override + public XmlStringBuilder toXML() { + XmlStringBuilder xml = new XmlStringBuilder(this); + xml.rightAngleBracket(); + xml.condEmptyElement(required, "required"); + xml.closeElement(this); + return xml; + } + +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/StreamOpen.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/StreamOpen.java new file mode 100644 index 000000000..62ef580a3 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/StreamOpen.java @@ -0,0 +1,57 @@ +/** + * + * Copyright © 2014 Florian Schmaus + * + * 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.packet; + +import org.jivesoftware.smack.util.XmlStringBuilder; + +/** + * + */ +public class StreamOpen extends FullStreamElement { + + public static final String ELEMENT = "stream:stream"; + public static final String NAMESPACE = "jabber:client"; + public static final String VERSION = "1.0"; + + private final String service; + + public StreamOpen(String service) { + this.service = service; + } + + @Override + public String getNamespace() { + return NAMESPACE; + } + + @Override + public String getElementName() { + return ELEMENT; + } + + @Override + public XmlStringBuilder toXML() { + XmlStringBuilder xml = new XmlStringBuilder(this); + xml.attribute("to", service); + xml.attribute("xmlns:stream", "http://etherx.jabber.org/streams"); + xml.attribute("version", VERSION); + xml.rightAngleBracket(); + return xml; + } + +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/TopLevelStreamElement.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/TopLevelStreamElement.java new file mode 100644 index 000000000..f978f0418 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/TopLevelStreamElement.java @@ -0,0 +1,26 @@ +/** + * + * Copyright © 2014 Florian Schmaus + * + * 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.packet; + +/** + * A XMPP top level stream element. This is either a stanza ({@link Packet}) or + * just a plain stream element ({@link PlainStreamElement}). + */ +public abstract class TopLevelStreamElement implements Element { + +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/XMPPError.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/XMPPError.java index 9f33a403e..4546b546a 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/packet/XMPPError.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/XMPPError.java @@ -72,6 +72,10 @@ public class XMPPError { private String message; private List applicationExtensions = null; + public XMPPError(String condition) { + this(new Condition(condition)); + } + /** * Creates a new error with the specified condition inferring the type. * If the Condition is predefined, client code should be like: diff --git a/smack-core/src/main/java/org/jivesoftware/smack/provider/ProviderManager.java b/smack-core/src/main/java/org/jivesoftware/smack/provider/ProviderManager.java index 540efb7d8..53c853b35 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/provider/ProviderManager.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/provider/ProviderManager.java @@ -23,6 +23,7 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.jivesoftware.smack.packet.IQ; +import org.jxmpp.util.XmppStringUtils; /** * Manages providers for parsing custom XML sub-documents of XMPP packets. Two types of @@ -111,13 +112,13 @@ public final class ProviderManager { public static void addLoader(ProviderLoader loader) { if (loader.getIQProviderInfo() != null) { for (IQProviderInfo info : loader.getIQProviderInfo()) { - iqProviders.put(getProviderKey(info.getElementName(), info.getNamespace()), info.getProvider()); + iqProviders.put(getKey(info.getElementName(), info.getNamespace()), info.getProvider()); } } if (loader.getExtensionProviderInfo() != null) { for (ExtensionProviderInfo info : loader.getExtensionProviderInfo()) { - extensionProviders.put(getProviderKey(info.getElementName(), info.getNamespace()), info.getProvider()); + extensionProviders.put(getKey(info.getElementName(), info.getNamespace()), info.getProvider()); } } } @@ -143,7 +144,7 @@ public final class ProviderManager { * @return the IQ provider. */ public static Object getIQProvider(String elementName, String namespace) { - String key = getProviderKey(elementName, namespace); + String key = getKey(elementName, namespace); return iqProviders.get(key); } @@ -176,7 +177,7 @@ public final class ProviderManager { throw new IllegalArgumentException("Provider must be an IQProvider " + "or a Class instance sublcassing IQ."); } - String key = getProviderKey(elementName, namespace); + String key = getKey(elementName, namespace); iqProviders.put(key, provider); } @@ -189,7 +190,7 @@ public final class ProviderManager { * @param namespace the XML namespace. */ public static void removeIQProvider(String elementName, String namespace) { - String key = getProviderKey(elementName, namespace); + String key = getKey(elementName, namespace); iqProviders.remove(key); } @@ -213,7 +214,7 @@ public final class ProviderManager { * @return the extenion provider. */ public static Object getExtensionProvider(String elementName, String namespace) { - String key = getProviderKey(elementName, namespace); + String key = getKey(elementName, namespace); return extensionProviders.get(key); } @@ -233,7 +234,7 @@ public final class ProviderManager { throw new IllegalArgumentException("Provider must be a PacketExtensionProvider " + "or a Class instance."); } - String key = getProviderKey(elementName, namespace); + String key = getKey(elementName, namespace); extensionProviders.put(key, provider); } @@ -246,7 +247,7 @@ public final class ProviderManager { * @param namespace the XML namespace. */ public static void removeExtensionProvider(String elementName, String namespace) { - String key = getProviderKey(elementName, namespace); + String key = getKey(elementName, namespace); extensionProviders.remove(key); } @@ -261,14 +262,7 @@ public final class ProviderManager { return Collections.unmodifiableCollection(extensionProviders.values()); } - /** - * Returns a String key for a given element name and namespace. - * - * @param elementName the element name. - * @param namespace the namespace. - * @return a unique key for the element name and namespace pair. - */ - private static String getProviderKey(String elementName, String namespace) { - return elementName + '#' + namespace; + private static String getKey(String elementName, String namespace) { + return XmppStringUtils.generateKey(elementName, namespace); } } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLErrorException.java b/smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLErrorException.java index 756a1cfa5..528086f95 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLErrorException.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLErrorException.java @@ -20,7 +20,7 @@ import java.util.HashMap; import java.util.Map; import org.jivesoftware.smack.XMPPException; -import org.jivesoftware.smack.sasl.packet.SaslStanzas.SASLFailure; +import org.jivesoftware.smack.sasl.packet.SaslStreamElements.SASLFailure; public class SASLErrorException extends XMPPException { diff --git a/smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLMechanism.java b/smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLMechanism.java index 661a72220..ae3bd0bb3 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLMechanism.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLMechanism.java @@ -19,8 +19,8 @@ package org.jivesoftware.smack.sasl; import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.SmackException.NotConnectedException; import org.jivesoftware.smack.XMPPConnection; -import org.jivesoftware.smack.sasl.packet.SaslStanzas.AuthMechanism; -import org.jivesoftware.smack.sasl.packet.SaslStanzas.Response; +import org.jivesoftware.smack.sasl.packet.SaslStreamElements.AuthMechanism; +import org.jivesoftware.smack.sasl.packet.SaslStreamElements.Response; import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.smack.util.stringencoder.Base64; @@ -181,7 +181,7 @@ public abstract class SASLMechanism implements Comparable { authenticationText = "="; } // Send the authentication to the server - connection.sendPacket(new AuthMechanism(getName(), authenticationText)); + connection.send(new AuthMechanism(getName(), authenticationText)); } /** @@ -218,7 +218,7 @@ public abstract class SASLMechanism implements Comparable { } // Send the authentication to the server - connection.sendPacket(responseStanza); + connection.send(responseStanza); } protected byte[] evaluateChallenge(byte[] challenge) throws SmackException { diff --git a/smack-core/src/main/java/org/jivesoftware/smack/sasl/packet/SaslStanzas.java b/smack-core/src/main/java/org/jivesoftware/smack/sasl/packet/SaslStreamElements.java similarity index 89% rename from smack-core/src/main/java/org/jivesoftware/smack/sasl/packet/SaslStanzas.java rename to smack-core/src/main/java/org/jivesoftware/smack/sasl/packet/SaslStreamElements.java index 2321045a3..8ecd0f1bf 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/sasl/packet/SaslStanzas.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/sasl/packet/SaslStreamElements.java @@ -16,18 +16,18 @@ */ package org.jivesoftware.smack.sasl.packet; -import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.packet.PlainStreamElement; import org.jivesoftware.smack.sasl.SASLError; import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.smack.util.XmlStringBuilder; -public class SaslStanzas { +public class SaslStreamElements { public static final String NAMESPACE = "urn:ietf:params:xml:ns:xmpp-sasl"; /** * Initiating SASL authentication by select a mechanism. */ - public static class AuthMechanism extends Packet { + public static class AuthMechanism extends PlainStreamElement { public static final String ELEMENT = "auth"; private final String mechanism; @@ -55,9 +55,9 @@ public class SaslStanzas { } /** - * A SASL challenge stanza. + * A SASL challenge stream element. */ - public static class Challenge extends Packet { + public static class Challenge extends PlainStreamElement { public static final String ELEMENT = "challenge"; private final String data; @@ -77,9 +77,9 @@ public class SaslStanzas { } /** - * A SASL response stanza. + * A SASL response stream element. */ - public static class Response extends Packet { + public static class Response extends PlainStreamElement { public static final String ELEMENT = "response"; private final String authenticationText; @@ -103,15 +103,15 @@ public class SaslStanzas { } /** - * A SASL success stanza. + * A SASL success stream element. */ - public static class Success extends Packet { + public static class Success extends PlainStreamElement { public static final String ELEMENT = "success"; final private String data; /** - * Construct a new SASL success stanza with optional additional data for the SASL layer + * Construct a new SASL success stream element with optional additional data for the SASL layer * (RFC6120 6.3.10) * * @param data additional data for the SASL layer or null @@ -140,9 +140,9 @@ public class SaslStanzas { } /** - * A SASL failure stanza. + * A SASL failure stream element. */ - public static class SASLFailure extends Packet { + public static class SASLFailure extends PlainStreamElement { public static final String ELEMENT = "failure"; private final SASLError saslError; diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/DNSUtil.java b/smack-core/src/main/java/org/jivesoftware/smack/util/DNSUtil.java index 0027e7a85..c08096726 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/DNSUtil.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/DNSUtil.java @@ -123,15 +123,20 @@ public class DNSUtil { } else { srvDomain = domain; } - List srvRecords = dnsResolver.lookupSRVRecords(srvDomain); - if (LOGGER.isLoggable(Level.FINE)) { - String logMessage = "Resolved SRV RR for " + srvDomain + ":"; - for (SRVRecord r : srvRecords) - logMessage += " " + r; - LOGGER.fine(logMessage); + try { + List srvRecords = dnsResolver.lookupSRVRecords(srvDomain); + if (LOGGER.isLoggable(Level.FINE)) { + String logMessage = "Resolved SRV RR for " + srvDomain + ":"; + for (SRVRecord r : srvRecords) + logMessage += " " + r; + LOGGER.fine(logMessage); + } + List sortedRecords = sortSRVRecords(srvRecords); + addresses.addAll(sortedRecords); + } + catch (Exception e) { + LOGGER.log(Level.WARNING, "Exception while resolving SRV records for " + domain, e); } - List sortedRecords = sortSRVRecords(srvRecords); - addresses.addAll(sortedRecords); // Step two: Add the hostname to the end of the list addresses.add(new HostAddress(domain)); diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/PacketParserUtils.java b/smack-core/src/main/java/org/jivesoftware/smack/util/PacketParserUtils.java index 99399070d..a1643ec69 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/PacketParserUtils.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/PacketParserUtils.java @@ -23,6 +23,7 @@ import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; +import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; @@ -30,6 +31,7 @@ import java.util.logging.Level; import java.util.logging.Logger; import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.compress.packet.Compress; import org.jivesoftware.smack.packet.Bind; import org.jivesoftware.smack.packet.DefaultPacketExtension; import org.jivesoftware.smack.packet.IQ; @@ -39,12 +41,13 @@ import org.jivesoftware.smack.packet.PacketExtension; import org.jivesoftware.smack.packet.Presence; import org.jivesoftware.smack.packet.Registration; import org.jivesoftware.smack.packet.RosterPacket; +import org.jivesoftware.smack.packet.StartTls; import org.jivesoftware.smack.packet.StreamError; import org.jivesoftware.smack.packet.XMPPError; import org.jivesoftware.smack.provider.IQProvider; import org.jivesoftware.smack.provider.PacketExtensionProvider; import org.jivesoftware.smack.provider.ProviderManager; -import org.jivesoftware.smack.sasl.packet.SaslStanzas.SASLFailure; +import org.jivesoftware.smack.sasl.packet.SaslStreamElements.SASLFailure; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserFactory; @@ -63,9 +66,7 @@ public class PacketParserUtils { } public static XmlPullParser getParserFor(Reader reader) throws XmlPullParserException, IOException { - XmlPullParser parser = newXmppParser(); - parser.setInput(reader); - + XmlPullParser parser = newXmppParser(reader); // Wind the parser forward to the first start tag int event = parser.getEventType(); while (event != XmlPullParser.START_TAG) { @@ -116,20 +117,17 @@ public class PacketParserUtils { * @throws Exception */ public static Packet parseStanza(XmlPullParser parser, XMPPConnection connection) throws Exception { - final int eventType = parser.getEventType(); - if (eventType != XmlPullParser.START_TAG) { - throw new IllegalArgumentException("Parser not at start tag"); - } + assert(parser.getEventType() == XmlPullParser.START_TAG); final String name = parser.getName(); switch (name) { - case "message": + case Message.ELEMENT: return parseMessage(parser); - case "iq": + case IQ.ELEMENT: return parseIQ(parser, connection); - case "presence": + case Presence.ELEMENT: return parsePresence(parser); default: - return null; + throw new IllegalArgumentException("Can only parse message, iq or presence, not " + name); } } @@ -151,6 +149,25 @@ public class PacketParserUtils { return parser; } + /** + * 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. + *

+ * + * @param reader + * @return A suitable XmlPullParser for XMPP parsing + * @throws XmlPullParserException + */ + public static XmlPullParser newXmppParser(Reader reader) throws XmlPullParserException { + XmlPullParser parser = newXmppParser(); + parser.setInput(reader); + return parser; + } + /** * Parses a message packet. * @@ -525,8 +542,7 @@ public class PacketParserUtils { else if (elementName.equals("query") && namespace.equals("jabber:iq:register")) { iqPacket = parseRegistration(parser); } - else if (elementName.equals("bind") && - namespace.equals("urn:ietf:params:xml:ns:xmpp-bind")) { + else if (elementName.equals(Bind.ELEMENT) && namespace.equals(Bind.NAMESPACE)) { iqPacket = parseResourceBinding(parser); } // Otherwise, see if there is a registered provider for @@ -688,25 +704,35 @@ public class PacketParserUtils { return registration; } - private static Bind parseResourceBinding(XmlPullParser parser) throws IOException, - XmlPullParserException { + public static Bind parseResourceBinding(XmlPullParser parser) throws IOException, + XmlPullParserException { + assert (parser.getEventType() == XmlPullParser.START_TAG); + int initalDepth = parser.getDepth(); + String name; Bind bind = null; - boolean done = false; - while (!done) { + outerloop: while (true) { int eventType = parser.next(); - if (eventType == XmlPullParser.START_TAG) { - if (parser.getName().equals("resource")) { + switch (eventType) { + case XmlPullParser.START_TAG: + name = parser.getName(); + switch (name) { + case "resource": bind = Bind.newSet(parser.nextText()); - } - else if (parser.getName().equals("jid")) { + break; + case "jid": bind = Bind.newResult(parser.nextText()); + break; } - } else if (eventType == XmlPullParser.END_TAG) { - if (parser.getName().equals(Bind.ELEMENT)) { - done = true; + break; + case XmlPullParser.END_TAG: + name = parser.getName(); + if (name.equals(Bind.ELEMENT) && parser.getDepth() == initalDepth) { + break outerloop; } + break; } } + assert (parser.getEventType() == XmlPullParser.END_TAG); return bind; } @@ -715,9 +741,11 @@ public class PacketParserUtils { * * @param parser the XML parser, positioned at the start of the mechanisms stanza. * @return a collection of Stings with the mechanisms included in the mechanisms stanza. - * @throws Exception if an exception occurs while parsing the stanza. + * @throws IOException + * @throws XmlPullParserException */ - public static Collection parseMechanisms(XmlPullParser parser) throws Exception { + public static Collection parseMechanisms(XmlPullParser parser) + throws XmlPullParserException, IOException { List mechanisms = new ArrayList(); boolean done = false; while (!done) { @@ -739,32 +767,42 @@ public class PacketParserUtils { } /** - * Parse the available compression methods reported from the server. + * Parse the Compression Feature reported from the server. * * @param parser the XML parser, positioned at the start of the compression stanza. - * @return a collection of Stings with the methods included in the compression stanza. + * @return The CompressionFeature stream element * @throws XmlPullParserException if an exception occurs while parsing the stanza. */ - public static Collection parseCompressionMethods(XmlPullParser parser) - throws IOException, XmlPullParserException { - List methods = new ArrayList(); - boolean done = false; - while (!done) { + public static Compress.Feature parseCompressionFeature(XmlPullParser parser) + throws IOException, XmlPullParserException { + assert (parser.getEventType() == XmlPullParser.START_TAG); + String name; + final int initialDepth = parser.getDepth(); + List methods = new LinkedList(); + outerloop: while (true) { int eventType = parser.next(); - - if (eventType == XmlPullParser.START_TAG) { - String elementName = parser.getName(); - if (elementName.equals("method")) { + switch (eventType) { + case XmlPullParser.START_TAG: + name = parser.getName(); + switch (name) { + case "method": methods.add(parser.nextText()); + break; } - } - else if (eventType == XmlPullParser.END_TAG) { - if (parser.getName().equals("compression")) { - done = true; + break; + case XmlPullParser.END_TAG: + name = parser.getName(); + switch (name) { + case Compress.Feature.ELEMENT: + if (parser.getDepth() == initialDepth) { + break outerloop; + } } } } - return methods; + assert (parser.getEventType() == XmlPullParser.END_TAG); + assert (parser.getDepth() == initialDepth); + return new Compress.Feature(methods); } /** @@ -840,21 +878,16 @@ public class PacketParserUtils { * @throws Exception if an exception occurs while parsing the packet. */ public static XMPPError parseError(XmlPullParser parser) throws Exception { - final String errorNamespace = "urn:ietf:params:xml:ns:xmpp-stanzas"; String type = null; String message = null; String condition = null; List extensions = new ArrayList(); // Parse the error header - for (int i=0; i queue = new LinkedBlockingQueue(); + private final BlockingQueue queue = new LinkedBlockingQueue(); public DummyConnection() { this(new ConnectionConfiguration("example.com")); @@ -178,6 +179,14 @@ public class DummyConnection extends AbstractXMPPConnection { authenticated = true; } + @Override + public void send(PlainStreamElement element) { + if (SmackConfiguration.DEBUG_ENABLED) { + System.out.println("[SEND]: " + element.toXML()); + } + queue.add(element); + } + @Override protected void sendPacketInternal(Packet packet) { if (SmackConfiguration.DEBUG_ENABLED) { @@ -204,7 +213,7 @@ public class DummyConnection extends AbstractXMPPConnection { * @throws InterruptedException */ public Packet getSentPacket() throws InterruptedException { - return queue.poll(); + return (Packet) queue.poll(); } /** @@ -217,7 +226,7 @@ public class DummyConnection extends AbstractXMPPConnection { * @throws InterruptedException */ public Packet getSentPacket(int wait) throws InterruptedException { - return queue.poll(wait, TimeUnit.SECONDS); + return (Packet) queue.poll(wait, TimeUnit.SECONDS); } /** diff --git a/smack-core/src/test/java/org/jivesoftware/smack/RosterVersioningTest.java b/smack-core/src/test/java/org/jivesoftware/smack/RosterVersioningTest.java index 62ab2307d..6e4980fed 100644 --- a/smack-core/src/test/java/org/jivesoftware/smack/RosterVersioningTest.java +++ b/smack-core/src/test/java/org/jivesoftware/smack/RosterVersioningTest.java @@ -69,8 +69,6 @@ public class RosterVersioningTest { connection = new DummyConnection(conf); connection.connect(); - connection.setRosterVersioningSupported(); - connection.login("rostertest", "secret"); } diff --git a/smack-core/src/test/java/org/jivesoftware/smack/test/util/TestUtils.java b/smack-core/src/test/java/org/jivesoftware/smack/test/util/TestUtils.java index d5d4d7c5d..4da7b1f05 100644 --- a/smack-core/src/test/java/org/jivesoftware/smack/test/util/TestUtils.java +++ b/smack-core/src/test/java/org/jivesoftware/smack/test/util/TestUtils.java @@ -47,8 +47,7 @@ final public class TestUtils { public static XmlPullParser getParser(Reader reader, String startTag) { XmlPullParser parser; try { - parser = PacketParserUtils.newXmppParser(); - parser.setInput(reader); + parser = PacketParserUtils.newXmppParser(reader); if (startTag == null) { return parser; } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/socks5/packet/Bytestream.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/socks5/packet/Bytestream.java index b7abb17f5..b6febddd4 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/socks5/packet/Bytestream.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/socks5/packet/Bytestream.java @@ -20,8 +20,8 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import org.jivesoftware.smack.packet.Element; import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.NamedElement; import org.jivesoftware.smack.util.XmlStringBuilder; /** @@ -265,7 +265,7 @@ public class Bytestream extends IQ { * * @author Alexander Wenckus */ - public static class StreamHost implements Element { + public static class StreamHost implements NamedElement { public static String ELEMENTNAME = "streamhost"; @@ -343,7 +343,7 @@ public class Bytestream extends IQ { * * @author Alexander Wenckus */ - public static class StreamHostUsed implements Element { + public static class StreamHostUsed implements NamedElement { public static String ELEMENTNAME = "streamhost-used"; @@ -385,7 +385,7 @@ public class Bytestream extends IQ { * * @author Alexander Wenckus */ - public static class Activate implements Element { + public static class Activate implements NamedElement { public static String ELEMENTNAME = "activate"; diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/caps/EntityCapsManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/caps/EntityCapsManager.java index e2110b616..cba5ed67c 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/caps/EntityCapsManager.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/caps/EntityCapsManager.java @@ -26,6 +26,7 @@ import org.jivesoftware.smack.PacketInterceptor; import org.jivesoftware.smack.PacketListener; import org.jivesoftware.smack.XMPPConnectionRegistry; import org.jivesoftware.smack.XMPPException.XMPPErrorException; +import org.jivesoftware.smack.packet.CapsExtension; import org.jivesoftware.smack.packet.IQ; import org.jivesoftware.smack.packet.Packet; import org.jivesoftware.smack.packet.PacketExtension; @@ -38,7 +39,6 @@ import org.jivesoftware.smack.filter.PacketExtensionFilter; import org.jivesoftware.smack.util.Cache; import org.jivesoftware.smack.util.stringencoder.Base64; import org.jivesoftware.smackx.caps.cache.EntityCapsPersistentCache; -import org.jivesoftware.smackx.caps.packet.CapsExtension; import org.jivesoftware.smackx.disco.NodeInformationProvider; import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; import org.jivesoftware.smackx.disco.packet.DiscoverInfo; @@ -74,8 +74,8 @@ import java.security.NoSuchAlgorithmException; public class EntityCapsManager extends Manager { private static final Logger LOGGER = Logger.getLogger(EntityCapsManager.class.getName()); - public static final String NAMESPACE = "http://jabber.org/protocol/caps"; - public static final String ELEMENT = "c"; + public static final String NAMESPACE = CapsExtension.NAMESPACE; + public static final String ELEMENT = CapsExtension.ELEMENT; private static final Map SUPPORTED_HASHES = new HashMap(); private static String DEFAULT_ENTITY_NODE = "http://www.igniterealtime.org/projects/smack"; @@ -239,6 +239,17 @@ public class EntityCapsManager extends Manager { CAPS_CACHE.clear(); } + private static void addCapsExtensionInfo(String from, CapsExtension capsExtension) { + String hash = capsExtension.getHash().toLowerCase(Locale.US); + if (!SUPPORTED_HASHES.containsKey(hash)) + return; + + String node = capsExtension.getNode(); + String ver = capsExtension.getVer(); + + JID_TO_NODEVER_CACHE.put(from, new NodeVerHash(node, ver, hash)); + } + private final Queue lastLocalCapsVersions = new ConcurrentLinkedQueue(); private final ServiceDiscoveryManager sdm; @@ -258,6 +269,20 @@ public class EntityCapsManager extends Manager { instances.put(connection, this); connection.addConnectionListener(new AbstractConnectionListener() { + @Override + public void connected(XMPPConnection connection) { + // It's not clear when a server would report the caps stream + // feature, so we try to process it after we are connected and + // once after we are authenticated. + processCapsStreamFeatureIfAvailable(connection); + } + @Override + public void authenticated(XMPPConnection connection) { + // It's not clear when a server would report the caps stream + // feature, so we try to process it after we are connected and + // once after we are authenticated. + processCapsStreamFeatureIfAvailable(connection); + } @Override public void connectionClosed() { presenceSend = false; @@ -266,6 +291,16 @@ public class EntityCapsManager extends Manager { public void connectionClosedOnError(Exception e) { presenceSend = false; } + + private void processCapsStreamFeatureIfAvailable(XMPPConnection connection) { + CapsExtension capsExtension = connection.getFeature( + CapsExtension.ELEMENT, CapsExtension.NAMESPACE); + if (capsExtension == null) { + return; + } + String from = connection.getServiceName(); + addCapsExtensionInfo(from, capsExtension); + } }); // This calculates the local entity caps version @@ -282,18 +317,9 @@ public class EntityCapsManager extends Manager { if (!entityCapsEnabled()) return; - CapsExtension ext = (CapsExtension) packet.getExtension(EntityCapsManager.ELEMENT, - EntityCapsManager.NAMESPACE); - - String hash = ext.getHash().toLowerCase(Locale.US); - if (!SUPPORTED_HASHES.containsKey(hash)) - return; - + CapsExtension capsExtension = CapsExtension.from(packet); String from = packet.getFrom(); - String node = ext.getNode(); - String ver = ext.getVer(); - - JID_TO_NODEVER_CACHE.put(from, new NodeVerHash(node, ver, hash)); + addCapsExtensionInfo(from, capsExtension); } }, PRESENCES_WITH_CAPS); diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/caps/provider/CapsExtensionProvider.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/caps/provider/CapsExtensionProvider.java index d6ec9c017..2862313f6 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/caps/provider/CapsExtensionProvider.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/caps/provider/CapsExtensionProvider.java @@ -19,10 +19,10 @@ package org.jivesoftware.smackx.caps.provider; import java.io.IOException; import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.packet.CapsExtension; import org.jivesoftware.smack.packet.PacketExtension; import org.jivesoftware.smack.provider.PacketExtensionProvider; import org.jivesoftware.smackx.caps.EntityCapsManager; -import org.jivesoftware.smackx.caps.packet.CapsExtension; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/delay/provider/DelayInformationProvider.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/delay/provider/DelayInformationProvider.java index 4876e856e..1a6010343 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/delay/provider/DelayInformationProvider.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/delay/provider/DelayInformationProvider.java @@ -28,7 +28,6 @@ import org.jxmpp.util.XmppDateTime; */ public class DelayInformationProvider extends AbstractDelayInformationProvider { - @SuppressWarnings("deprecation") @Override protected Date parseDate(String string) throws ParseException { return XmppDateTime.parseXEP0082Date(string); diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/packet/Destroy.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/packet/Destroy.java index f1bbf8929..a24391bcf 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/packet/Destroy.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/packet/Destroy.java @@ -16,7 +16,7 @@ */ package org.jivesoftware.smackx.muc.packet; -import org.jivesoftware.smack.packet.Element; +import org.jivesoftware.smack.packet.NamedElement; import org.jivesoftware.smack.util.XmlStringBuilder; /** @@ -26,7 +26,7 @@ import org.jivesoftware.smack.util.XmlStringBuilder; * * @author Gaston Dombiak */ -public class Destroy implements Element { +public class Destroy implements NamedElement { public static final String ELEMENT = "destroy"; private String reason; diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/packet/MUCInitialPresence.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/packet/MUCInitialPresence.java index ce568e3a3..260c26ebc 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/packet/MUCInitialPresence.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/packet/MUCInitialPresence.java @@ -17,7 +17,7 @@ package org.jivesoftware.smackx.muc.packet; -import org.jivesoftware.smack.packet.Element; +import org.jivesoftware.smack.packet.NamedElement; import org.jivesoftware.smack.packet.Packet; import org.jivesoftware.smack.packet.PacketExtension; import org.jivesoftware.smack.util.XmlStringBuilder; @@ -119,7 +119,7 @@ public class MUCInitialPresence implements PacketExtension { * * @author Gaston Dombiak */ - public static class History implements Element { + public static class History implements NamedElement { public static final String ELEMENT = "history"; diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/packet/MUCItem.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/packet/MUCItem.java index 354f5ccc4..9922599c7 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/packet/MUCItem.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/packet/MUCItem.java @@ -16,8 +16,8 @@ */ package org.jivesoftware.smackx.muc.packet; -import org.jivesoftware.smack.packet.Element; import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.NamedElement; import org.jivesoftware.smack.util.XmlStringBuilder; import org.jivesoftware.smackx.muc.MUCAffiliation; import org.jivesoftware.smackx.muc.MUCRole; @@ -27,7 +27,7 @@ import org.jivesoftware.smackx.muc.MUCRole; * * @author Gaston Dombiak */ -public class MUCItem implements Element { +public class MUCItem implements NamedElement { public static final String ELEMENT = IQ.ITEM; private final MUCAffiliation affiliation; diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/packet/MUCUser.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/packet/MUCUser.java index febe48b89..f5ed8fe85 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/packet/MUCUser.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/packet/MUCUser.java @@ -17,7 +17,7 @@ package org.jivesoftware.smackx.muc.packet; -import org.jivesoftware.smack.packet.Element; +import org.jivesoftware.smack.packet.NamedElement; import org.jivesoftware.smack.packet.Packet; import java.util.HashMap; @@ -218,7 +218,7 @@ public class MUCUser implements PacketExtension { * * @author Gaston Dombiak */ - public static class Invite implements Element { + public static class Invite implements NamedElement { public static final String ELEMENT ="invite"; private String reason; @@ -304,7 +304,7 @@ public class MUCUser implements PacketExtension { * * @author Gaston Dombiak */ - public static class Decline implements Element { + public static class Decline implements NamedElement { public static final String ELEMENT = "decline"; private String reason; @@ -390,7 +390,7 @@ public class MUCUser implements PacketExtension { * * @author Gaston Dombiak */ - public static class Status implements Element { + public static class Status implements NamedElement { public static final String ELEMENT = "status"; private static final Map statusMap = new HashMap(8); diff --git a/smack-extensions/src/test/java/org/jivesoftware/smackx/delay/provider/DelayInformationTest.java b/smack-extensions/src/test/java/org/jivesoftware/smackx/delay/provider/DelayInformationTest.java index d2df005ec..7722e81f2 100644 --- a/smack-extensions/src/test/java/org/jivesoftware/smackx/delay/provider/DelayInformationTest.java +++ b/smack-extensions/src/test/java/org/jivesoftware/smackx/delay/provider/DelayInformationTest.java @@ -20,6 +20,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import java.io.IOException; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; @@ -27,6 +28,8 @@ import java.util.GregorianCalendar; import java.util.Properties; import java.util.TimeZone; +import javax.xml.parsers.FactoryConfigurationError; + import org.jivesoftware.smack.packet.Presence; import org.jivesoftware.smack.util.PacketParserUtils; import org.jxmpp.util.XmppDateTime; @@ -35,14 +38,17 @@ import org.jivesoftware.smackx.delay.DelayInformationManager; import org.jivesoftware.smackx.delay.packet.DelayInformation; import org.junit.Test; import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; import com.jamesmurty.utils.XMLBuilder; public class DelayInformationTest extends InitExtensions { + private static final Calendar calendar = new GregorianCalendar(2002, 9 - 1, 10, 23, 8, 25); private static Properties outputProperties = new Properties(); static { outputProperties.put(javax.xml.transform.OutputKeys.OMIT_XML_DECLARATION, "yes"); + calendar.setTimeZone(TimeZone.getTimeZone("UTC")); } @Test @@ -95,8 +101,6 @@ public class DelayInformationTest extends InitExtensions { DelayInformationProvider p = new DelayInformationProvider(); DelayInformation delayInfo; String control; - GregorianCalendar calendar = new GregorianCalendar(2002, 9 - 1, 10, 23, 8, 25); - calendar.setTimeZone(TimeZone.getTimeZone("UTC")); // XEP-0082 date format control = XMLBuilder.create("delay") @@ -132,6 +136,14 @@ public class DelayInformationTest extends InitExtensions { delayInfo = (DelayInformation) p.parseExtension(PacketParserUtils.getParserFor(control)); assertEquals(calendar.getTime(), delayInfo.getStamp()); + } + + @Test + public void legacyDateFormatsTest() throws FactoryConfigurationError, XmlPullParserException, IOException, Exception { + LegacyDelayInformationProvider p = new LegacyDelayInformationProvider(); + DelayInformation delayInfo; + + String control; // XEP-0091 date format control = XMLBuilder.create("x") diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XMPPTCPConnection.java b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XMPPTCPConnection.java index a3b90dcc6..140e476ea 100644 --- a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XMPPTCPConnection.java +++ b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XMPPTCPConnection.java @@ -18,33 +18,64 @@ package org.jivesoftware.smack.tcp; import org.jivesoftware.smack.AbstractXMPPConnection; import org.jivesoftware.smack.ConnectionConfiguration; +import org.jivesoftware.smack.ConnectionConfiguration.SecurityMode; import org.jivesoftware.smack.ConnectionCreationListener; import org.jivesoftware.smack.ConnectionListener; +import org.jivesoftware.smack.PacketListener; import org.jivesoftware.smack.SmackConfiguration; import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.SmackException.AlreadyConnectedException; import org.jivesoftware.smack.SmackException.AlreadyLoggedInException; +import org.jivesoftware.smack.SmackException.NoResponseException; import org.jivesoftware.smack.SmackException.NotConnectedException; import org.jivesoftware.smack.SmackException.ConnectionException; -import org.jivesoftware.smack.SmackException.SecurityNotPossibleException; import org.jivesoftware.smack.SmackException.SecurityRequiredException; +import org.jivesoftware.smack.SynchronizationPoint; import org.jivesoftware.smack.XMPPException.StreamErrorException; import org.jivesoftware.smack.XMPPConnection; import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.XMPPException.XMPPErrorException; +import org.jivesoftware.smack.compress.packet.Compressed; import org.jivesoftware.smack.compression.XMPPInputOutputStream; +import org.jivesoftware.smack.filter.PacketFilter; +import org.jivesoftware.smack.compress.packet.Compress; +import org.jivesoftware.smack.packet.Element; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.packet.StreamOpen; import org.jivesoftware.smack.packet.Packet; import org.jivesoftware.smack.packet.Presence; +import org.jivesoftware.smack.packet.StartTls; import org.jivesoftware.smack.parsing.ParsingExceptionCallback; import org.jivesoftware.smack.parsing.UnparsablePacket; -import org.jivesoftware.smack.sasl.packet.SaslStanzas.Challenge; -import org.jivesoftware.smack.sasl.packet.SaslStanzas.SASLFailure; -import org.jivesoftware.smack.sasl.packet.SaslStanzas.Success; +import org.jivesoftware.smack.sasl.packet.SaslStreamElements; +import org.jivesoftware.smack.sasl.packet.SaslStreamElements.Challenge; +import org.jivesoftware.smack.sasl.packet.SaslStreamElements.SASLFailure; +import org.jivesoftware.smack.sasl.packet.SaslStreamElements.Success; +import org.jivesoftware.smack.packet.PlainStreamElement; +import org.jivesoftware.smack.packet.XMPPError; +import org.jivesoftware.smack.tcp.sm.SMUtils; +import org.jivesoftware.smack.tcp.sm.StreamManagementException; +import org.jivesoftware.smack.tcp.sm.StreamManagementException.StreamManagementNotEnabledException; +import org.jivesoftware.smack.tcp.sm.StreamManagementException.StreamIdDoesNotMatchException; +import org.jivesoftware.smack.tcp.sm.packet.StreamManagement; +import org.jivesoftware.smack.tcp.sm.packet.StreamManagement.AckAnswer; +import org.jivesoftware.smack.tcp.sm.packet.StreamManagement.AckRequest; +import org.jivesoftware.smack.tcp.sm.packet.StreamManagement.Enable; +import org.jivesoftware.smack.tcp.sm.packet.StreamManagement.Enabled; +import org.jivesoftware.smack.tcp.sm.packet.StreamManagement.Failed; +import org.jivesoftware.smack.tcp.sm.packet.StreamManagement.Resume; +import org.jivesoftware.smack.tcp.sm.packet.StreamManagement.Resumed; +import org.jivesoftware.smack.tcp.sm.packet.StreamManagement.StreamManagementFeature; +import org.jivesoftware.smack.tcp.sm.predicates.Predicate; +import org.jivesoftware.smack.tcp.sm.provider.ParseStreamManagement; import org.jivesoftware.smack.util.ArrayBlockingQueueWithShutdown; import org.jivesoftware.smack.util.PacketParserUtils; +import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.smack.util.TLSUtils; import org.jivesoftware.smack.util.dns.HostAddress; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; -import org.xmlpull.v1.XmlPullParserFactory; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.KeyManager; @@ -56,7 +87,6 @@ import javax.security.auth.callback.CallbackHandler; import javax.security.auth.callback.PasswordCallback; import java.io.BufferedReader; -import java.io.BufferedWriter; import java.io.ByteArrayInputStream; import java.io.FileInputStream; import java.io.IOException; @@ -64,7 +94,6 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; -import java.io.UnsupportedEncodingException; import java.io.Writer; import java.lang.reflect.Constructor; import java.net.Socket; @@ -77,24 +106,32 @@ import java.security.Provider; import java.security.Security; import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; +import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; +import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Locale; -import java.util.concurrent.atomic.AtomicBoolean; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; import java.util.logging.Level; import java.util.logging.Logger; /** * Creates a socket connection to a XMPP server. This is the default connection - * to a Jabber server and is specified in the XMPP Core (RFC 3920). + * to a XMPP server and is specified in the XMPP Core (RFC 6120). * * @see XMPPConnection * @author Matt Tucker */ public class XMPPTCPConnection extends AbstractXMPPConnection { + private static final int QUEUE_SIZE = 500; private static final Logger LOGGER = Logger.getLogger(XMPPTCPConnection.class.getName()); /** @@ -103,35 +140,104 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { private Socket socket; private String connectionID = null; - private String user = null; private boolean connected = false; + /** + * + */ + private boolean disconnectedButResumeable = false; + // socketClosed is used concurrent // by XMPPTCPConnection, PacketReader, PacketWriter private volatile boolean socketClosed = false; - private boolean anonymous = false; private boolean usingTLS = false; private ParsingExceptionCallback parsingExceptionCallback = SmackConfiguration.getDefaultParsingExceptionCallback(); - private PacketWriter packetWriter; - private PacketReader packetReader; + /** + * Protected access level because of unit test purposes + */ + protected PacketWriter packetWriter; /** - * Collection of available stream compression methods offered by the server. + * Protected access level because of unit test purposes */ - private Collection compressionMethods; + protected PacketReader packetReader; + + private final SynchronizationPoint initalOpenStreamSend = new SynchronizationPoint(this); /** - * Set to true by packet writer if the server acknowledged the compression + * */ - private boolean serverAckdCompression = false; + private final SynchronizationPoint maybeCompressFeaturesReceived = new SynchronizationPoint( + this); /** - * Lock for the wait()/notify() pattern for the compression negotiation + * */ - private final Object compressionLock = new Object(); + private final SynchronizationPoint compressSyncPoint = new SynchronizationPoint( + this); + + private static boolean useSmDefault = false; + + private static boolean useSmResumptionDefault = true; + + /** + * The stream ID of the stream that is currently resumable, ie. the stream we hold the state + * for in {@link #clientHandledStanzasCount}, {@link #serverHandledStanzasCount} and + * {@link #unacknowledgedStanzas}. + */ + private String smSessionId; + + private final SynchronizationPoint smResumedSyncPoint = new SynchronizationPoint( + this); + + private final SynchronizationPoint smEnabledSyncPoint = new SynchronizationPoint( + this); + + /** + * The client's preferred maximum resumption time in seconds. + */ + private int smClientMaxResumptionTime = -1; + + /** + * The server's preferred maximum resumption time in seconds. + */ + private int smServerMaxResumptimTime = -1; + + /** + * Indicates whether Stream Management (XEP-198) should be used if it's supported by the server. + */ + private boolean useSm = useSmDefault; + private boolean useSmResumption = useSmResumptionDefault; + private long serverHandledStanzasCount = 0; + private long clientHandledStanzasCount = 0; + private BlockingQueue unacknowledgedStanzas; + + /** + * This listeners are invoked for every stanza that got acknowledged. + *

+ * We use a {@link ConccurrentLinkedQueue} here in order to allow the listeners to remove + * themselves after they have been invoked. + *

+ */ + private final Collection stanzaAcknowledgedListeners = new ConcurrentLinkedQueue(); + + /** + * This listeners are invoked for a acknowledged stanza that has the given stanza ID. They will + * only be invoked once and automatically removed after that. + */ + private final Map idStanzaAcknowledgedListeners = new ConcurrentHashMap(); + + /** + * Predicates that determine if an stream management ack should be requested from the server. + *

+ * We use a linked hash set here, so that the order how the predicates are added matches the + * order in which they are invoked in order to determine if an ack request should be send or not. + *

+ */ + private final Set requestAckPredicates = new LinkedHashSet(); /** * Creates a new connection to the specified XMPP server. A DNS SRV lookup will be @@ -250,9 +356,13 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { if (!isConnected()) { throw new NotConnectedException(); } - if (authenticated) { + if (authenticated && !disconnectedButResumeable) { throw new AlreadyLoggedInException(); } + + // Wait with SASL auth until the SASL mechanisms have been received + saslFeatureReceived.checkIfSuccessOrWaitOrThrow(); + // Do partial version of nameprep on the username. if (username != null) { username = username.toLowerCase(Locale.US).trim(); @@ -276,43 +386,52 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { useCompression(); } - // Set the user. - String response = bindResourceAndEstablishSession(resource); - if (response != null) { - this.user = response; - // Update the serviceName with the one returned by the server - setServiceName(response); - } - else { - this.user = username + "@" + getServiceName(); - if (resource != null) { - this.user += "/" + resource; + if (isSmResumptionPossible()) { + smResumedSyncPoint.sendAndWaitForResponse(new Resume(clientHandledStanzasCount, smSessionId)); + if (smResumedSyncPoint.wasSuccessful()) { + // We successfully resumed the stream, be done here + afterSuccessfulLogin(false, true); + return; } + // SM resumption failed, what Smack does here is to report success of + // lastFeaturesReceived in case of sm resumption was answered with 'failed' so that + // normal resource binding can be tried. + LOGGER.fine("Stream resumption failed, continuing with normal stream establishment process"); } - // Indicate that we're now authenticated. - authenticated = true; - anonymous = false; + bindResourceAndEstablishSession(resource); + + List previouslyUnackedStanzas = new LinkedList(); + if (unacknowledgedStanzas != null) { + // There was a previous connection with SM enabled but that was either not resumable or + // failed to resume. Make sure that we (re-)send the unacknowledged stanzas. + unacknowledgedStanzas.drainTo(previouslyUnackedStanzas); + } + if (isSmAvailable() && useSm) { + // Remove what is maybe left from previously stream managed sessions + unacknowledgedStanzas = new ArrayBlockingQueue(QUEUE_SIZE); + clientHandledStanzasCount = 0; + serverHandledStanzasCount = 0; + // XEP-198 3. Enabling Stream Management. If the server response to 'Enable' is 'Failed' + // then this is a non recoverable error and we therefore throw an exception. + smEnabledSyncPoint.sendAndWaitForResponseOrThrow(new Enable(useSmResumption, smClientMaxResumptionTime)); + synchronized (requestAckPredicates) { + if (requestAckPredicates.isEmpty()) { + // Assure that we have at lest one predicate set up that so that we request acks + // for the server and eventually flush some stanzas from the unacknowledged + // stanza queue + requestAckPredicates.add(Predicate.forMessagesOrAfter5Stanzas()); + } + } + } + // (Re-)send the stanzas *after* we tried to enable SM + for (Packet stanza : previouslyUnackedStanzas) { + sendPacketInternal(stanza); + } // Stores the authentication for future reconnection setLoginInfo(username, password, resource); - - // If debugging is enabled, change the the debug window title to include the - // name we are now logged-in as. - // If DEBUG_ENABLED was set to true AFTER the connection was created the debugger - // will be null - if (config.isDebuggerEnabled() && debugger != null) { - debugger.userHasLogged(user); - } - callConnectionAuthenticatedListener(); - - // Set presence to online. It is important that this is done after - // callConnectionAuthenticatedListener(), as this call will also - // eventually load the roster. And we should load the roster before we - // send the initial presence. - if (config.isSendPresence()) { - sendPacket(new Presence(Presence.Type.available)); - } + afterSuccessfulLogin(false, false); } @Override @@ -324,6 +443,9 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { throw new AlreadyLoggedInException(); } + // Wait with SASL auth until the SASL mechanisms have been received + saslFeatureReceived.checkIfSuccessOrWaitOrThrow(); + if (saslAuthentication.hasAnonymousAuthentication()) { saslAuthentication.authenticateAnonymously(); } @@ -331,32 +453,14 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { throw new SmackException("No anonymous SASL authentication mechanism available"); } - String response = bindResourceAndEstablishSession(null); - // Set the user value. - this.user = response; - // Update the serviceName with the one returned by the server - setServiceName(response); - // If compression is enabled then request the server to use stream compression if (config.isCompressionEnabled()) { useCompression(); } - // Set presence to online. - sendPacket(new Presence(Presence.Type.available)); + bindResourceAndEstablishSession(null); - // Indicate that we're now authenticated. - authenticated = true; - anonymous = true; - - // If debugging is enabled, change the the debug window title to include the - // name we are now logged-in as. - // If DEBUG_ENABLED was set to true AFTER the connection was created the debugger - // will be null - if (config.isDebuggerEnabled() && debugger != null) { - debugger.userHasLogged(user); - } - callConnectionAuthenticatedListener(); + afterSuccessfulLogin(true, false); } @Override @@ -378,22 +482,28 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { return authenticated; } - @Override - public boolean isAnonymous() { - return anonymous; - } - /** * Shuts the current connection down. After this method returns, the connection must be ready * for re-use by connect. */ @Override protected void shutdown() { + shutdown(false); + } + + /** + * Performs an unclean disconnect and shutdown of the connection. Does not send a closing stream stanza. + */ + public void instantShutdown() { + shutdown(true); + } + + private void shutdown(boolean instant) { if (packetReader != null) { packetReader.shutdown(); } if (packetWriter != null) { - packetWriter.shutdown(); + packetWriter.shutdown(instant); } // Set socketClosed to true. This will cause the PacketReader @@ -408,16 +518,42 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { } setWasAuthenticated(authenticated); - authenticated = false; - connected = false; - usingTLS = false; + // If we are able to resume the stream, then don't set + // connected/authenticated/usingTLS to false since we like behave like we are still + // connected (e.g. sendPacket should not throw a NotConnectedException). + if (isSmResumptionPossible() && instant) { + disconnectedButResumeable = true; + } else { + authenticated = false; + connected = false; + usingTLS = false; + disconnectedButResumeable = false; + } reader = null; writer = null; + maybeCompressFeaturesReceived.init(); + compressSyncPoint.init(); + smResumedSyncPoint.init(); + smEnabledSyncPoint.init(); + initalOpenStreamSend.init(); + } + + @Override + public void send(PlainStreamElement element) throws NotConnectedException { + packetWriter.sendStreamElement(element); } @Override protected void sendPacketInternal(Packet packet) throws NotConnectedException { - packetWriter.sendPacket(packet); + packetWriter.sendStreamElement(packet); + if (isSmEnabled()) { + for (PacketFilter requestAckPredicate : requestAckPredicates) { + if (requestAckPredicate.accept(packet)) { + requestSmAcknowledgementInternal(); + break; + } + } + } } private void connectUsingConfiguration(ConnectionConfiguration config) throws SmackException, IOException { @@ -474,7 +610,6 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { private void initConnection() throws SmackException, IOException { boolean isFirstInitialization = packetReader == null || packetWriter == null; compressionHandler = null; - serverAckdCompression = false; // Set the reader and writer instance variables initReaderAndWriter(); @@ -493,16 +628,11 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { } } } - else { - packetWriter.init(); - packetReader.init(); - } - // Start the packet writer. This will open a XMPP stream to the server - packetWriter.startup(); + packetWriter.init(); // Start the packet reader. The startup() method will block until we - // get an opening stream packet back from server. - packetReader.startup(); + // get an opening stream packet back from server + packetReader.init(); // Make note of the fact that we're now connected. connected = true; @@ -516,74 +646,39 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { } catch (SmackException ex) { - // An exception occurred in setting up the connection. - shutdown(); - // Everything stoppped. Now throw the exception. + // An exception occurred in setting up the connection. Note that + // it's important here that we do an instant shutdown here, as this + // will not send a closing stream element, which will destroy + // Stream Management state on the server, which is not what we want. + instantShutdown(); + // Everything stopped. Now throw the exception. throw ex; } } - private void initReaderAndWriter() throws IOException { + private void initReaderAndWriter() throws IOException, SmackException { try { - if (compressionHandler == null) { - reader = - new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8")); - writer = new BufferedWriter( - new OutputStreamWriter(socket.getOutputStream(), "UTF-8")); - } - else { - try { - OutputStream os = compressionHandler.getOutputStream(socket.getOutputStream()); - writer = new BufferedWriter(new OutputStreamWriter(os, "UTF-8")); - - InputStream is = compressionHandler.getInputStream(socket.getInputStream()); - reader = new BufferedReader(new InputStreamReader(is, "UTF-8")); - } - catch (Exception e) { - LOGGER.log(Level.WARNING, "initReaderAndWriter()", e); - compressionHandler = null; - reader = new BufferedReader( - new InputStreamReader(socket.getInputStream(), "UTF-8")); - writer = new BufferedWriter( - new OutputStreamWriter(socket.getOutputStream(), "UTF-8")); - } + InputStream is = socket.getInputStream(); + OutputStream os = socket.getOutputStream(); + if (compressionHandler != null) { + is = compressionHandler.getInputStream(is); + os = compressionHandler.getOutputStream(os); } + // OutputStreamWriter is already buffered, no need to wrap it into a BufferedWriter + writer = new OutputStreamWriter(os, "UTF-8"); + reader = new BufferedReader(new InputStreamReader(is, "UTF-8")); } - catch (UnsupportedEncodingException ioe) { - throw new IllegalStateException(ioe); + catch (IOException e) { + throw e; + } + catch (Exception e) { + throw new SmackException(e); } // If debugging is enabled, we open a window and write out all network traffic. initDebugger(); } - /*********************************************** - * TLS code below - **********************************************/ - - /** - * Notification message saying that the server supports TLS so confirm the server that we - * want to secure the connection. - * - * @param required true when the server indicates that TLS is required. - * @throws IOException if an exception occurs. - */ - private void startTLSReceived(boolean required) throws IOException { - if (required && config.getSecurityMode() == - ConnectionConfiguration.SecurityMode.disabled) { - notifyConnectionError(new IllegalStateException( - "TLS required by server but not allowed by connection configuration")); - return; - } - - if (config.getSecurityMode() == ConnectionConfiguration.SecurityMode.disabled) { - // Do not secure the connection using TLS since TLS was disabled - return; - } - writer.write(""); - writer.flush(); - } - /** * The server has indicated that TLS negotiation can start. We now need to secure the * existing plain connection and perform a handshake. This method won't return until the @@ -595,11 +690,10 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { * @throws KeyStoreException * @throws UnrecoverableKeyException * @throws KeyManagementException - * @throws SecurityNotPossibleException - * + * @throws SmackException * @throws Exception if an exception occurs. */ - private void proceedTLSReceived() throws NoSuchAlgorithmException, CertificateException, IOException, KeyStoreException, NoSuchProviderException, UnrecoverableKeyException, KeyManagementException, SecurityNotPossibleException { + private void proceedTLSReceived() throws NoSuchAlgorithmException, CertificateException, IOException, KeyStoreException, NoSuchProviderException, UnrecoverableKeyException, KeyManagementException, SmackException { SSLContext context = this.config.getCustomSSLContext(); KeyStore ks = null; KeyManager[] kms = null; @@ -686,31 +780,8 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { throw new CertificateException("Hostname verification of certificate failed. Certificate does not authenticate " + getServiceName()); } - //if (((SSLSocket) socket).getWantClientAuth()) { - // System.err.println("XMPPConnection wants client auth"); - //} - //else if (((SSLSocket) socket).getNeedClientAuth()) { - // System.err.println("XMPPConnection needs client auth"); - //} - //else { - // System.err.println("XMPPConnection does not require client auth"); - // } // Set that TLS was successful usingTLS = true; - - // Set the new writer to use - packetWriter.setWriter(writer); - // Send a new opening stream to the server - packetWriter.openStream(); - } - - /** - * Sets the available stream compression methods offered by the server. - * - * @param methods compression methods offered by the server. - */ - private void setAvailableCompressionMethods(Collection methods) { - compressionMethods = methods; } /** @@ -720,96 +791,49 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { * */ private XMPPInputOutputStream maybeGetCompressionHandler() { - if (compressionMethods != null) { - for (XMPPInputOutputStream handler : SmackConfiguration.getCompresionHandlers()) { + Compress.Feature compression = getFeature(Compress.Feature.ELEMENT, Compress.NAMESPACE); + if (compression == null) { + // Server does not support compression + return null; + } + for (XMPPInputOutputStream handler : SmackConfiguration.getCompresionHandlers()) { String method = handler.getCompressionMethod(); - if (compressionMethods.contains(method)) + if (compression.getMethods().contains(method)) return handler; - } } return null; } @Override public boolean isUsingCompression() { - return compressionHandler != null && serverAckdCompression; + return compressionHandler != null && compressSyncPoint.wasSuccessful(); } /** + *

* Starts using stream compression that will compress network traffic. Traffic can be * reduced up to 90%. Therefore, stream compression is ideal when using a slow speed network * connection. However, the server and the client will need to use more CPU time in order to * un/compress network data so under high load the server performance might be affected. - *

+ *

*

* Stream compression has to have been previously offered by the server. Currently only the * zlib method is supported by the client. Stream compression negotiation has to be done - * before authentication took place.

- *

+ * before authentication took place. + *

* - * @return true if stream compression negotiation was successful. - * @throws IOException if the compress stanza could not be send + * @throws NotConnectedException + * @throws XMPPException + * @throws NoResponseException */ - private boolean useCompression() throws IOException { + private void useCompression() throws NotConnectedException, NoResponseException, XMPPException { + maybeCompressFeaturesReceived.checkIfSuccessOrWait(); // If stream compression was offered by the server and we want to use // compression then send compression request to the server - if (authenticated) { - throw new IllegalStateException("Compression should be negotiated before authentication."); - } - if ((compressionHandler = maybeGetCompressionHandler()) != null) { - synchronized (compressionLock) { - requestStreamCompression(compressionHandler.getCompressionMethod()); - // Wait until compression is being used or a timeout happened - try { - compressionLock.wait(getPacketReplyTimeout()); - } - catch (InterruptedException e) { - // Ignore. - } - } - return isUsingCompression(); - } - return false; - } - - /** - * Request the server that we want to start using stream compression. When using TLS - * then negotiation of stream compression can only happen after TLS was negotiated. If TLS - * compression is being used the stream compression should not be used. - * @throws IOException if the compress stanza could not be send - */ - private void requestStreamCompression(String method) throws IOException { - writer.write(""); - writer.write("" + method + ""); - writer.flush(); - } - - /** - * Start using stream compression since the server has acknowledged stream compression. - * - * @throws IOException if there is an exception starting stream compression. - */ - private void startStreamCompression() throws IOException { - serverAckdCompression = true; - // Initialize the reader and writer with the new secured version - initReaderAndWriter(); - - // Set the new writer to use - packetWriter.setWriter(writer); - // Send a new opening stream to the server - packetWriter.openStream(); - // Notify that compression is being used - streamCompressionNegotiationDone(); - } - - /** - * Notifies the XMPP connection that stream compression negotiation is done so that the - * connection process can proceed. - */ - private void streamCompressionNegotiationDone() { - synchronized (compressionLock) { - compressionLock.notify(); + compressSyncPoint.sendAndWaitForResponseOrThrow(new Compress(compressionHandler.getCompressionMethod())); + } else { + LOGGER.warning("Could not enable compression because no matching handler/method pair was found"); } } @@ -827,16 +851,16 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { */ @Override protected void connectInternal() throws SmackException, IOException, XMPPException { + if (connected && !disconnectedButResumeable) { + throw new AlreadyConnectedException(); + } // Establishes the connection, readers and writers connectUsingConfiguration(config); - // TODO is there a case where connectUsing.. does not throw an exception but connected is - // still false? - if (connected) { - callConnectionConnectedListener(); - } + callConnectionConnectedListener(); + // Automatically makes the login if the user was previously connected successfully // to the server and the connection was terminated abruptly - if (connected && wasAuthenticated) { + if (wasAuthenticated) { // Make the login if (isAnonymous()) { // Make the anonymous login @@ -858,10 +882,10 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { private synchronized void notifyConnectionError(Exception e) { // Listeners were already notified of the exception, return right here. if ((packetReader == null || packetReader.done) && - (packetWriter == null || packetWriter.done)) return; + (packetWriter == null || packetWriter.done())) return; // Closes the connection temporary. A reconnection is possible - shutdown(); + instantShutdown(); // Notify connection listeners of the error. callConnectionClosedOnErrorListener(e); @@ -884,26 +908,85 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { } } + /** + * For unit testing purposes + * + * @param writer + */ + protected void setWriter(Writer writer) { + this.writer = writer; + } + + @Override + protected void parseFeaturesSubclass(String name, String namespace, XmlPullParser parser) { + switch(name) { + case StreamManagementFeature.ELEMENT: + if (namespace.equals(StreamManagement.NAMESPACE)) { + addStreamFeature(StreamManagementFeature.INSTANCE); + } else { + LOGGER.fine("Unsupported Stream Management version: " + namespace); + } + break; + } + } + + @Override + protected void afterFeaturesReceived() throws SecurityRequiredException, NotConnectedException { + StartTls startTlsFeature = getFeature(StartTls.ELEMENT, StartTls.NAMESPACE); + if (startTlsFeature != null) { + if (startTlsFeature.required() && config.getSecurityMode() == SecurityMode.disabled) { + notifyConnectionError(new SecurityRequiredException( + "TLS required by server but not allowed by connection configuration")); + return; + } + + if (config.getSecurityMode() == ConnectionConfiguration.SecurityMode.disabled) { + // Do not secure the connection using TLS since TLS was disabled + return; + } + send(new StartTls()); + } + // If TLS is required but the server doesn't offer it, disconnect + // from the server and throw an error. First check if we've already negotiated TLS + // and are secure, however (features get parsed a second time after TLS is established). + if (!isSecureConnection() && startTlsFeature == null + && getConfiguration().getSecurityMode() == SecurityMode.required) { + throw new SecurityRequiredException(); + } + + if (getSASLAuthentication().authenticationSuccessful()) { + // If we have received features after the SASL has been successfully completed, then we + // have also *maybe* received, as it is an optional feature, the compression feature + // from the server. + maybeCompressFeaturesReceived.reportSuccess(); + } + } + + /** + * Resets the parser using the latest connection's reader. Reseting the parser is necessary + * when the plain connection has been secured or when a new opening stream element is going + * to be sent by the server. + * + * @throws SmackException if the parser could not be reset. + */ + void openStream() throws SmackException { + send(new StreamOpen(getServiceName())); + try { + packetReader.parser = PacketParserUtils.newXmppParser(reader); + } + catch (XmlPullParserException e) { + throw new SmackException(e); + } + } + protected class PacketReader { private Thread readerThread; - private XmlPullParser parser; - - /** - * Set to true if the last features stanza from the server has been parsed. A XMPP connection - * handshake can invoke multiple features stanzas, e.g. when TLS is activated a second feature - * stanza is send by the server. This is set to true once the last feature stanza has been - * parsed. - */ - private volatile boolean lastFeaturesParsed; + XmlPullParser parser; private volatile boolean done; - PacketReader() throws SmackException { - this.init(); - } - /** * Initializes the reader in order to be used. The reader is initialized during the * first connection and when reconnecting due to an abruptly disconnection. @@ -912,44 +995,16 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { */ void init() throws SmackException { done = false; - lastFeaturesParsed = false; readerThread = new Thread() { public void run() { - parsePackets(this); + parsePackets(); } }; readerThread.setName("Smack Packet Reader (" + getConnectionCounter() + ")"); readerThread.setDaemon(true); - - resetParser(); - } - - /** - * Starts the packet reader thread and returns once a connection to the server - * has been established or if the server's features could not be parsed within - * the connection's PacketReplyTimeout. - * - * @throws IOException - * @throws SmackException - */ - synchronized void startup() throws IOException, SmackException { readerThread.start(); - - try { - // Wait until either: - // - the servers last features stanza has been parsed - // - an exception is thrown while parsing - // - the timeout occurs - wait(getPacketReplyTimeout()); - } - catch (InterruptedException ie) { - // Ignore. - } - if (!lastFeaturesParsed) { - throwConnectionExceptionOrNoResponse(); - } - } + } /** * Shuts the packet reader down. This method simply sets the 'done' flag to true. @@ -958,56 +1013,45 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { done = true; } - /** - * Resets the parser using the latest connection's reader. Reseting the parser is necessary - * when the plain connection has been secured or when a new opening stream element is going - * to be sent by the server. - * - * @throws SmackException if the parser could not be reset. - */ - private void resetParser() throws SmackException { - try { - parser = XmlPullParserFactory.newInstance().newPullParser(); - parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); - parser.setInput(getReader()); - } - catch (XmlPullParserException e) { - throw new SmackException(e); - } - } - /** * Parse top-level packets in order to process them further. * * @param thread the thread that is being used by the reader to parse incoming packets. */ - private void parsePackets(Thread thread) { + private void parsePackets() { try { + initalOpenStreamSend.checkIfSuccessOrWait(); int eventType = parser.getEventType(); do { if (eventType == XmlPullParser.START_TAG) { - int parserDepth = parser.getDepth(); - String name = parser.getName(); - ParsingExceptionCallback callback = getParsingExceptionCallback(); - Packet packet; - try { - packet = PacketParserUtils.parseStanza(parser, XMPPTCPConnection.this); - } catch (Exception e) { - CharSequence content = PacketParserUtils.parseContentDepth(parser, parserDepth); - UnparsablePacket message = new UnparsablePacket(content, e); - if (callback != null) { - callback.handleUnparsablePacket(message); + final String name = parser.getName(); + switch (name) { + case Message.ELEMENT: + case IQ.ELEMENT: + case Presence.ELEMENT: + int parserDepth = parser.getDepth(); + Packet packet; + try { + packet = PacketParserUtils.parseStanza(parser, + XMPPTCPConnection.this); + } + catch (Exception e) { + ParsingExceptionCallback callback = getParsingExceptionCallback(); + CharSequence content = PacketParserUtils.parseContentDepth(parser, + parserDepth); + UnparsablePacket message = new UnparsablePacket(content, e); + if (callback != null) { + callback.handleUnparsablePacket(message); + } + continue; + } finally { + clientHandledStanzasCount = SMUtils.incrementHeight(clientHandledStanzasCount); + reportStanzaReceived(); } - continue; - } - if (packet != null) { processPacket(packet); - reportStanzaReceived(); - } - // We found an opening stream. Record information about it, then notify - // the connectionID lock so that the packet reader startup can finish. - else if (name.equals("stream")) { - // Ensure the correct jabber:client namespace is being used. + break; + case "stream": + // We found an opening stream. if ("jabber:client".equals(parser.getNamespace(null))) { // Get the connection id. for (int i=0; i stanzasToResend = new LinkedList(); + stanzasToResend.addAll(unacknowledgedStanzas); + for (Packet stanza : stanzasToResend) { + packetWriter.sendStreamElement(stanza); + } + smResumedSyncPoint.reportSuccess(); + smEnabledSyncPoint.reportSuccess(); + LOGGER.fine("Stream Management (XEP-198): Stream resumed"); + break; + case AckAnswer.ELEMENT: + AckAnswer ackAnswer = ParseStreamManagement.ackAnswer(parser); + processHandledCount(ackAnswer.getHandledCount()); + break; + case AckRequest.ELEMENT: + // AckRequest stanzas are trival, no need to parse them + if (smEnabledSyncPoint.wasSuccessful()) { + packetWriter.sendStreamElement(new AckAnswer(clientHandledStanzasCount)); + } else { + LOGGER.warning("SM Ack Request received while SM is not enabled"); + } + break; + default: + LOGGER.warning("Unkown top level stream element: " + name); + break; } } else if (eventType == XmlPullParser.END_TAG) { @@ -1096,218 +1219,120 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { } } eventType = parser.next(); - } while (!done && eventType != XmlPullParser.END_DOCUMENT && thread == readerThread); + } while (!done && eventType != XmlPullParser.END_DOCUMENT); } catch (Exception e) { // The exception can be ignored if the the connection is 'done' // or if the it was caused because the socket got closed if (!(done || isSocketClosed())) { - synchronized(this) { - this.notify(); - } // Close the connection and notify connection listeners of the // error. notifyConnectionError(e); } } } - - private void parseFeatures(XmlPullParser parser) throws Exception { - boolean startTLSReceived = false; - boolean startTLSRequired = false; - boolean done = false; - while (!done) { - int eventType = parser.next(); - - if (eventType == XmlPullParser.START_TAG) { - if (parser.getName().equals("starttls")) { - startTLSReceived = true; - } - else if (parser.getName().equals("mechanisms")) { - // The server is reporting available SASL mechanisms. Store this information - // which will be used later while logging (i.e. authenticating) into - // the server - getSASLAuthentication().setAvailableSASLMethods( - PacketParserUtils.parseMechanisms(parser)); - } - else if (parser.getName().equals("bind")) { - // The server requires the client to bind a resource to the stream - serverRequiresBinding(); - } - // Set the entity caps node for the server if one is send - // See http://xmpp.org/extensions/xep-0115.html#stream - else if (parser.getName().equals("c")) { - String node = parser.getAttributeValue(null, "node"); - String ver = parser.getAttributeValue(null, "ver"); - if (ver != null && node != null) { - String capsNode = node + "#" + ver; - // In order to avoid a dependency from smack to smackx - // we have to set the services caps node in the connection - // and not directly in the EntityCapsManager - setServiceCapsNode(capsNode); - } - } - else if (parser.getName().equals("session")) { - // The server supports sessions - serverSupportsSession(); - } - else if (parser.getName().equals("ver")) { - if (parser.getNamespace().equals("urn:xmpp:features:rosterver")) { - setRosterVersioningSupported(); - } - } - else if (parser.getName().equals("compression")) { - // The server supports stream compression - setAvailableCompressionMethods(PacketParserUtils.parseCompressionMethods(parser)); - } - else if (parser.getName().equals("register")) { - serverSupportsAccountCreation(); - } - } - else if (eventType == XmlPullParser.END_TAG) { - if (parser.getName().equals("starttls")) { - // Confirm the server that we want to use TLS - startTLSReceived(startTLSRequired); - } - else if (parser.getName().equals("required") && startTLSReceived) { - startTLSRequired = true; - } - else if (parser.getName().equals("features")) { - done = true; - } - } - } - - // If TLS is required but the server doesn't offer it, disconnect - // from the server and throw an error. First check if we've already negotiated TLS - // and are secure, however (features get parsed a second time after TLS is established). - if (!isSecureConnection()) { - if (!startTLSReceived - && getConfiguration().getSecurityMode() == ConnectionConfiguration.SecurityMode.required) - { - throw new SecurityRequiredException(); - } - } - - // Release the lock after TLS has been negotiated or we are not interested in TLS. If the - // server announced TLS and we choose to use it, by sending 'starttls', which the server - // replied with 'proceed', the server is required to send a new stream features element that - // "MUST NOT include the STARTTLS feature" (RFC6120 5.4.3.3. 5.). We are therefore save to - // release the connection lock once either TLS is disabled or we received a features stanza - // without starttls. - if (!startTLSReceived - || getConfiguration().getSecurityMode() == ConnectionConfiguration.SecurityMode.disabled) - { - lastFeaturesParsed = true; - // This synchronized block prevents this thread from calling notify() before the other - // thread had called wait() (it would cause an Exception if wait() hadn't been called) - synchronized (this) { - notify(); - } - } - } } protected class PacketWriter { - public static final int QUEUE_SIZE = 500; + public static final int QUEUE_SIZE = XMPPTCPConnection.QUEUE_SIZE; - private final ArrayBlockingQueueWithShutdown queue = new ArrayBlockingQueueWithShutdown(QUEUE_SIZE, true); + private final ArrayBlockingQueueWithShutdown queue = new ArrayBlockingQueueWithShutdown( + QUEUE_SIZE, true); private Thread writerThread; - private Writer writer; - - private volatile boolean done; - - protected AtomicBoolean shutdownDone = new AtomicBoolean(false); /** - * Creates a new packet writer with the specified connection. + * Needs to be protected for unit testing purposes. */ - PacketWriter() { - init(); - } + protected SynchronizationPoint shutdownDone = new SynchronizationPoint( + XMPPTCPConnection.this); + + /** + * If set, the packet writer is shut down + */ + protected volatile Long shutdownTimestamp = null; + + private volatile boolean instantShutdown; /** * Initializes the writer in order to be used. It is called at the first connection and also * is invoked if the connection is disconnected by an error. */ void init() { - writer = getWriter(); - done = false; - shutdownDone.set(false); + shutdownDone.init(); + shutdownTimestamp = null; + + if (unacknowledgedStanzas != null) { + // It's possible that there are new stanzas in the writer queue that + // came in while we were disconnected but resumable, drain those into + // the unacknowledged queue so that they get resent now + drainWriterQueueToUnacknowledgedStanzas(); + } queue.start(); writerThread = new Thread() { public void run() { - writePackets(this); + writePackets(); } }; writerThread.setName("Smack Packet Writer (" + getConnectionCounter() + ")"); writerThread.setDaemon(true); + writerThread.start(); + } + + private boolean done() { + return shutdownTimestamp != null; } /** - * Sends the specified packet to the server. + * Sends the specified element to the server. * - * @param packet the packet to send. + * @param element the element to send. * @throws NotConnectedException */ - public void sendPacket(Packet packet) throws NotConnectedException { - if (done) { + protected void sendStreamElement(Element element) throws NotConnectedException { + if (done() && !isSmResumptionPossible()) { + // Don't throw a NotConnectedException is there is an resumable stream available throw new NotConnectedException(); } try { - queue.put(packet); + queue.put(element); } catch (InterruptedException ie) { throw new NotConnectedException(); } } - /** - * Starts the packet writer thread and opens a connection to the server. The - * packet writer will continue writing packets until {@link #shutdown} or an - * error occurs. - */ - void startup() { - writerThread.start(); - } - - void setWriter(Writer writer) { - this.writer = writer; - } - /** * Shuts down the packet writer. Once this method has been called, no further * packets will be written to the server. */ - void shutdown() { - done = true; + void shutdown(boolean instant) { + instantShutdown = instant; + shutdownTimestamp = System.currentTimeMillis(); queue.shutdown(); - synchronized(shutdownDone) { - if (!shutdownDone.get()) { - try { - shutdownDone.wait(getPacketReplyTimeout()); - } - catch (InterruptedException e) { - LOGGER.log(Level.WARNING, "shutdown", e); - } - } + try { + shutdownDone.checkIfSuccessOrWait(); + } + catch (NoResponseException e) { + LOGGER.log(Level.WARNING, "NoResponseException", e); } } /** - * Returns the next available packet from the queue for writing. + * Returns the next available element from the queue for writing. * - * @return the next packet for writing. + * @return the next element for writing. */ - private Packet nextPacket() { - if (done) { + private Element nextStreamElement() { + // TODO not sure if nextStreamElement and/or this done() condition still required. + // Couldn't this be done in writePackets too? + if (done()) { return null; } - Packet packet = null; + Element packet = null; try { packet = queue.take(); } @@ -1317,87 +1342,239 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { return packet; } - private void writePackets(Thread thisThread) { + private void writePackets() { try { - // Open the stream. openStream(); + initalOpenStreamSend.reportSuccess(); // Write out packets from the queue. - while (!done && (writerThread == thisThread)) { - Packet packet = nextPacket(); + while (!done()) { + Element packet = nextStreamElement(); if (packet != null) { + // Check if the stream element should be put to the unacknowledgedStanza + // queue. Note that we can not do the put() in sendPacketInternal() and the + // packet order is not stable at this point (sendPacketInternal() can be + // called concurrently). + if (isSmEnabled() && packet instanceof Packet) { + // If the unacknowledgedStanza queue is nearly full, request an new ack + // from the server in order to drain it + if (unacknowledgedStanzas.size() == 0.8 * XMPPTCPConnection.QUEUE_SIZE) { + writer.write(AckRequest.INSTANCE.toXML().toString()); + writer.flush(); + } + try { + unacknowledgedStanzas.put((Packet) packet); + } + catch (InterruptedException e) { + throw new IllegalStateException(e); + } + } writer.write(packet.toXML().toString()); - if (queue.isEmpty()) { writer.flush(); } } } - // Flush out the rest of the queue. If the queue is extremely large, it's possible - // we won't have time to entirely flush it before the socket is forced closed - // by the shutdown process. - try { - while (!queue.isEmpty()) { - Packet packet = queue.remove(); - writer.write(packet.toXML().toString()); - } - writer.flush(); - } - catch (Exception e) { - LOGGER.log(Level.WARNING, "Exception flushing queue during shutdown, ignore and continue", e); - } - - // Delete the queue contents (hopefully nothing is left). - queue.clear(); - - // Close the stream. - try { - writer.write(""); - writer.flush(); - } - catch (Exception e) { - LOGGER.log(Level.WARNING, "Exception writing closing stream element", e); - - } - finally { + if (!instantShutdown) { + // Flush out the rest of the queue. If the queue is extremely large, it's + // possible we won't have time to entirely flush it before the socket is forced + // closed by the shutdown process. try { - writer.close(); + while (!queue.isEmpty()) { + Element packet = queue.remove(); + writer.write(packet.toXML().toString()); + } + writer.flush(); } catch (Exception e) { - // Do nothing + LOGGER.log(Level.WARNING, + "Exception flushing queue during shutdown, ignore and continue", + e); } + + // Close the stream. + try { + writer.write(""); + writer.flush(); + } + catch (Exception e) { + LOGGER.log(Level.WARNING, "Exception writing closing stream element", e); + } + // Delete the queue contents (hopefully nothing is left). + queue.clear(); + } else if (instantShutdown && isSmEnabled()) { + // This was an instantShutdown and SM is enabled, drain all remaining stanzas + // into the unacknowledgedStanzas queue + drainWriterQueueToUnacknowledgedStanzas(); } - shutdownDone.set(true); - synchronized(shutdownDone) { - shutdownDone.notify(); + try { + writer.close(); } + catch (Exception e) { + // Do nothing + } + } - catch (IOException ioe) { + catch (Exception e) { // The exception can be ignored if the the connection is 'done' // or if the it was caused because the socket got closed - if (!(done || isSocketClosed())) { - shutdown(); - notifyConnectionError(ioe); + if (!(done() || isSocketClosed())) { + notifyConnectionError(e); + } else { + LOGGER.log(Level.FINE, "Ignoring Exception in writePackets()", e); } + } finally { + shutdownDone.reportSuccess(); } } - /** - * Sends to the server a new stream element. This operation may be requested several times - * so we need to encapsulate the logic in one place. This message will be sent while doing - * TLS, SASL and resource binding. - * - * @throws IOException If an error occurs while sending the stanza to the server. - */ - void openStream() throws IOException { - StringBuilder stream = new StringBuilder(); - stream.append(""); - writer.write(stream.toString()); - writer.flush(); + private void drainWriterQueueToUnacknowledgedStanzas() { + List elements = new ArrayList(queue.size()); + queue.drainTo(elements); + for (Element element : elements) { + if (element instanceof Packet) { + unacknowledgedStanzas.add((Packet) element); + } + } } } + + public static void setUseStreamManagementDefault(boolean useSmDefault) { + XMPPTCPConnection.useSmDefault = useSmDefault; + } + + public static void setUseStreamManagementResumptiodDefault(boolean useSmResupmptionDefault) { + XMPPTCPConnection.useSmResumptionDefault = useSmResupmptionDefault; + } + + public void setUseStreamManagement(boolean useSm) { + this.useSm = useSm; + } + + public void setUseStreamManagementResumption(boolean useSmResumption) { + this.useSmResumption = useSmResumption; + } + + /** + * Set the preferred resumption time in seconds. + * @param resumptionTime the preferred resumption time in seconds + */ + public void setPreferredResumptionTime(int resumptionTime) { + smClientMaxResumptionTime = resumptionTime; + } + + public boolean addRequestAckPredicate(PacketFilter predicate) { + synchronized (requestAckPredicates) { + return requestAckPredicates.add(predicate); + } + } + + public boolean removeRequestAckPredicate(PacketFilter predicate) { + synchronized (requestAckPredicates) { + return requestAckPredicates.remove(predicate); + } + } + + public void removeAllRequestAckPredicates() { + synchronized (requestAckPredicates) { + requestAckPredicates.clear(); + } + } + + public void requestSmAcknowledgement() throws StreamManagementNotEnabledException, NotConnectedException { + if (!isSmEnabled()) { + throw new StreamManagementException.StreamManagementNotEnabledException(); + } + requestSmAcknowledgementInternal(); + } + + private void requestSmAcknowledgementInternal() throws NotConnectedException { + packetWriter.sendStreamElement(AckRequest.INSTANCE); + } + + public void addStanzaAcknowledgedListener(PacketListener listener) { + stanzaAcknowledgedListeners.add(listener); + } + + public boolean removeStanzaAcknowledgedListener(PacketListener listener) { + return stanzaAcknowledgedListeners.remove(listener); + } + + public void removeAllStanzaAcknowledgedListeners() { + stanzaAcknowledgedListeners.clear(); + } + + public PacketListener addIdStanzaAcknowledgedListener(String id, PacketListener listener) { + return idStanzaAcknowledgedListeners.put(id, listener); + } + + public PacketListener removeIdStanzaAcknowledgedListener(String id) { + return idStanzaAcknowledgedListeners.remove(id); + } + + public void removeAllIdStanzaAcknowledgedListeners() { + idStanzaAcknowledgedListeners.clear(); + } + + public boolean isSmAvailable() { + return hasFeature(StreamManagementFeature.ELEMENT, StreamManagement.NAMESPACE); + } + + public boolean isSmEnabled() { + return smEnabledSyncPoint.wasSuccessful(); + } + + public boolean isDisconnectedButSmResumptionPossible() { + return disconnectedButResumeable && isSmResumptionPossible(); + } + + public boolean isSmResumptionPossible() { + // There is no resumable stream available + if (smSessionId == null) + return false; + + final Long shutdownTimestamp = packetWriter.shutdownTimestamp; + // Seems like we are already reconnected, report true + if (shutdownTimestamp == null) { + return true; + } + + // See if resumption time is over + long current = System.currentTimeMillis(); + int clientResumptionTime = smClientMaxResumptionTime > 0 ? smClientMaxResumptionTime : Integer.MAX_VALUE; + int serverResumptionTime = smServerMaxResumptimTime > 0 ? smServerMaxResumptimTime : Integer.MAX_VALUE; + long maxResumptionMillies = Math.max(clientResumptionTime, serverResumptionTime) * 1000; + if (shutdownTimestamp + maxResumptionMillies > current) { + return false; + } else { + return true; + } + } + + private void processHandledCount(long handledCount) throws NotConnectedException { + long ackedStanzasCount = SMUtils.calculateDelta(handledCount, serverHandledStanzasCount); + List ackedStanzas = new ArrayList( + handledCount <= Integer.MAX_VALUE ? (int) handledCount + : Integer.MAX_VALUE); + for (long i = 0; i < ackedStanzasCount; i++) { + Packet ackedStanza = unacknowledgedStanzas.poll(); + // If the server ack'ed a stanza, then it must be in the + // unacknowledged stanza queue. There can be no exception. + assert(ackedStanza != null); + ackedStanzas.add(ackedStanza); + } + for (Packet ackedStanza : ackedStanzas) { + for (PacketListener listener : stanzaAcknowledgedListeners) { + listener.processPacket(ackedStanza); + } + String id = ackedStanza.getPacketID(); + if (id != null) { + PacketListener listener = idStanzaAcknowledgedListeners.remove(id); + if (listener != null) { + listener.processPacket(ackedStanza); + } + } + } + serverHandledStanzasCount = handledCount; + } } diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/sm/SMUtils.java b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/sm/SMUtils.java new file mode 100644 index 000000000..49be69e99 --- /dev/null +++ b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/sm/SMUtils.java @@ -0,0 +1,50 @@ +/** + * + * Copyright © 2014 Florian Schmaus + * + * 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.tcp.sm; + +import java.math.BigInteger; + +public class SMUtils { + + private static long MASK_32_BIT = BigInteger.ONE.shiftLeft(32).subtract(BigInteger.ONE).longValue(); + + /** + * Quoting XEP-198 4.: + * "In the unlikely case that the number of stanzas handled during a stream management session exceeds the number + * of digits that can be represented by the unsignedInt datatype as specified in XML Schema Part 2 [10] + * (i.e., 2^32), the value of 'h' SHALL be reset from 2^32-1 back to zero (rather than being incremented to 2^32)." + * + * @param height + * @return the incremented height + */ + public static long incrementHeight(long height) { + return ++height & MASK_32_BIT; + } + + /** + * Calculates the delta of the last known stanza handled count and the new + * reported stanza handled count while considering that the new value may be + * wrapped after 2^32-1. + * + * @param reportedHandledCount + * @param lastKnownHandledCount + * @return the delta + */ + public static long calculateDelta(long reportedHandledCount, long lastKnownHandledCount) { + return (reportedHandledCount - lastKnownHandledCount) & MASK_32_BIT; + } +} diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/sm/StreamManagementException.java b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/sm/StreamManagementException.java new file mode 100644 index 000000000..179fb93f1 --- /dev/null +++ b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/sm/StreamManagementException.java @@ -0,0 +1,56 @@ +/** + * + * Copyright © 2014 Florian Schmaus + * + * 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.tcp.sm; + +import org.jivesoftware.smack.SmackException; + +public abstract class StreamManagementException extends SmackException { + + public StreamManagementException() { + } + + public StreamManagementException(String message) { + super(message); + } + + /** + * + */ + private static final long serialVersionUID = 3767590115788821101L; + + public static class StreamManagementNotEnabledException extends StreamManagementException { + + /** + * + */ + private static final long serialVersionUID = 2624821584352571307L; + + } + + public static class StreamIdDoesNotMatchException extends StreamManagementException { + + /** + * + */ + private static final long serialVersionUID = 1191073341336559621L; + + public StreamIdDoesNotMatchException(String expected, String got) { + super("Stream IDs do not match. Expected '" + expected + "', but got '" + got + "'"); + } + } +} + diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/sm/packet/StreamManagement.java b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/sm/packet/StreamManagement.java new file mode 100644 index 000000000..1fa8de461 --- /dev/null +++ b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/sm/packet/StreamManagement.java @@ -0,0 +1,344 @@ +/** + * + * Copyright © 2014 Florian Schmaus + * + * 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.tcp.sm.packet; + +import org.jivesoftware.smack.packet.FullStreamElement; +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smack.packet.XMPPError; +import org.jivesoftware.smack.util.XmlStringBuilder; + +public class StreamManagement { + public static final String NAMESPACE = "urn:xmpp:sm:3"; + + public static class StreamManagementFeature implements PacketExtension { + + public static final String ELEMENT = "sm"; + public static final StreamManagementFeature INSTANCE = new StreamManagementFeature(); + + private StreamManagementFeature() { + } + + @Override + public String getElementName() { + return ELEMENT; + } + + @Override + public String getNamespace() { + return NAMESPACE; + } + + @Override + public CharSequence toXML() { + XmlStringBuilder xml = new XmlStringBuilder(this); + xml.closeEmptyElement(); + return xml; + } + } + + private static abstract class AbstractEnable extends FullStreamElement { + + /** + * Preferred maximum resumption time in seconds (optional). + */ + protected int max = -1; + + protected boolean resume = false; + + protected void maybeAddResumeAttributeTo(XmlStringBuilder xml) { + if (resume) { + // XEP 198 never mentions the case where resume='false', it's either set to true or + // not set at all. We reflect this in this code part + xml.attribute("resume", "true"); + } + } + + protected void maybeAddMaxAttributeTo(XmlStringBuilder xml) { + if (max > 0) { + xml.attribute("max", Integer.toString(max)); + } + } + + public boolean isResumeSet() { + return resume; + } + + /** + * Return the max resumption time in seconds. + * @return the max resumption time in seconds + */ + public int getMaxResumptionTime() { + return max; + } + + @Override + public final String getNamespace() { + return NAMESPACE; + } + } + + public static class Enable extends AbstractEnable { + public static final String ELEMENT = "enable"; + + public static final Enable INSTANCE = new Enable(); + + private Enable() { + } + + public Enable(boolean resume) { + this.resume = resume; + } + + public Enable(boolean resume, int max) { + this(resume); + this.max = max; + } + + @Override + public CharSequence toXML() { + XmlStringBuilder xml = new XmlStringBuilder(this); + maybeAddResumeAttributeTo(xml); + maybeAddMaxAttributeTo(xml); + xml.closeEmptyElement(); + return xml; + } + + @Override + public String getElementName() { + return ELEMENT; + } + } + + /** + * A Stream Management 'enabled' element. + *

+ * Here is a full example, all attributes besides 'xmlns' are optional. + *

+ *
+     * {@code
+     * 
+     * }
+     * 
+ */ + public static class Enabled extends AbstractEnable { + public static final String ELEMENT = "enabled"; + + /** + * The stream id ("SM-ID") + */ + private final String id; + + /** + * The location where the server prefers reconnection. + */ + private final String location; + + public Enabled(String id, boolean resume) { + this(id, resume, null, -1); + } + + public Enabled(String id, boolean resume, String location, int max) { + this.id = id; + this.resume = resume; + this.location = location; + this.max = max; + } + + public String getId() { + return id; + } + + public String getLocation() { + return location; + } + + @Override + public CharSequence toXML() { + XmlStringBuilder xml = new XmlStringBuilder(this); + xml.optAttribute("id", id); + maybeAddResumeAttributeTo(xml); + xml.optAttribute("location", location); + maybeAddMaxAttributeTo(xml); + xml.closeEmptyElement(); + return xml; + } + + @Override + public String getElementName() { + return ELEMENT; + } + } + + public static class Failed extends FullStreamElement { + public static final String ELEMENT = "failed"; + + private XMPPError error; + + public Failed() { + } + + public Failed(XMPPError error) { + this.error = error; + } + + public XMPPError getXMPPError() { + return error; + } + + @Override + public CharSequence toXML() { + XmlStringBuilder xml = new XmlStringBuilder(this); + if (error != null) { + xml.rightAngleBracket(); + xml.append(error.toXML()); + xml.closeElement(ELEMENT); + } + else { + xml.closeEmptyElement(); + } + return xml; + } + + @Override + public String getNamespace() { + return NAMESPACE; + } + + @Override + public String getElementName() { + return ELEMENT; + } + + } + + private static abstract class AbstractResume extends FullStreamElement { + + private final long handledCount; + private final String previd; + + public AbstractResume(long handledCount, String previd) { + this.handledCount = handledCount; + this.previd = previd; + } + + public long getHandledCount() { + return handledCount; + } + + public String getPrevId() { + return previd; + } + + @Override + public final String getNamespace() { + return NAMESPACE; + } + + @Override + public final XmlStringBuilder toXML() { + XmlStringBuilder xml = new XmlStringBuilder(this); + xml.attribute("h", Long.toString(handledCount)); + xml.attribute("previd", previd); + xml.closeEmptyElement(); + return xml; + } + } + + public static class Resume extends AbstractResume { + public static final String ELEMENT = "resume"; + + public Resume(long handledCount, String previd) { + super(handledCount, previd); + } + + @Override + public String getElementName() { + return ELEMENT; + } + } + + public static class Resumed extends AbstractResume { + public static final String ELEMENT = "resumed"; + + public Resumed(long handledCount, String previd) { + super(handledCount, previd); + } + + @Override + public String getElementName() { + return ELEMENT; + } + } + + public static class AckAnswer extends FullStreamElement { + public static final String ELEMENT = "a"; + + private final long handledCount; + + public AckAnswer(long handledCount) { + this.handledCount = handledCount; + } + + public long getHandledCount() { + return handledCount; + } + + @Override + public CharSequence toXML() { + XmlStringBuilder xml = new XmlStringBuilder(this); + xml.attribute("h", Long.toString(handledCount)); + xml.closeEmptyElement(); + return xml; + } + + @Override + public String getNamespace() { + return NAMESPACE; + } + + @Override + public String getElementName() { + return ELEMENT; + } + } + + public static class AckRequest extends FullStreamElement { + public static final String ELEMENT = "r"; + public static final AckRequest INSTANCE = new AckRequest(); + + private AckRequest() { + } + + @Override + public CharSequence toXML() { + return '<' + ELEMENT + " xmlns='" + NAMESPACE + "'/>"; + } + + @Override + public String getNamespace() { + return NAMESPACE; + } + + @Override + public String getElementName() { + return ELEMENT; + } + } +} diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/sm/predicates/AfterXStanzas.java b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/sm/predicates/AfterXStanzas.java new file mode 100644 index 000000000..de9a9f006 --- /dev/null +++ b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/sm/predicates/AfterXStanzas.java @@ -0,0 +1,45 @@ +/** + * + * Copyright © 2014 Florian Schmaus + * + * 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.tcp.sm.predicates; + +import org.jivesoftware.smack.filter.PacketFilter; +import org.jivesoftware.smack.packet.Packet; + +public class AfterXStanzas implements PacketFilter { + + final int count; + int currentCount; + + public AfterXStanzas(int count) { + this.count = count; + currentCount = 0; + } + + @Override + public synchronized boolean accept(Packet packet) { + currentCount++; + if (currentCount == count) { + resetCounter(); + return true; + } + return false; + } + + public synchronized void resetCounter() { + currentCount = 0; + } +} diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/sm/predicates/ForEveryMessage.java b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/sm/predicates/ForEveryMessage.java new file mode 100644 index 000000000..d4a9b5661 --- /dev/null +++ b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/sm/predicates/ForEveryMessage.java @@ -0,0 +1,38 @@ +/** + * + * Copyright © 2014 Florian Schmaus + * + * 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.tcp.sm.predicates; + +import org.jivesoftware.smack.filter.PacketFilter; +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.packet.Packet; + +public class ForEveryMessage implements PacketFilter { + + public static final ForEveryMessage INSTANCE = new ForEveryMessage(); + + private ForEveryMessage() { + } + + @Override + public boolean accept(Packet packet) { + if (packet instanceof Message) { + return true; + } + return false; + } + +} diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/sm/predicates/ForEveryStanza.java b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/sm/predicates/ForEveryStanza.java new file mode 100644 index 000000000..93e9b81ec --- /dev/null +++ b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/sm/predicates/ForEveryStanza.java @@ -0,0 +1,34 @@ +/** + * + * Copyright © 2014 Florian Schmaus + * + * 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.tcp.sm.predicates; + +import org.jivesoftware.smack.filter.PacketFilter; +import org.jivesoftware.smack.packet.Packet; + +public class ForEveryStanza implements PacketFilter { + + public static final ForEveryStanza INSTANCE = new ForEveryStanza(); + + private ForEveryStanza() { + } + + @Override + public boolean accept(Packet packet) { + return true; + } + +} diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/sm/predicates/ForMatchingPredicateOrAfterXStanzas.java b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/sm/predicates/ForMatchingPredicateOrAfterXStanzas.java new file mode 100644 index 000000000..125b293e4 --- /dev/null +++ b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/sm/predicates/ForMatchingPredicateOrAfterXStanzas.java @@ -0,0 +1,40 @@ +/** + * + * Copyright © 2014 Florian Schmaus + * + * 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.tcp.sm.predicates; + +import org.jivesoftware.smack.filter.PacketFilter; +import org.jivesoftware.smack.packet.Packet; + +public class ForMatchingPredicateOrAfterXStanzas implements PacketFilter { + + private final PacketFilter predicate; + private final AfterXStanzas afterXStanzas; + + public ForMatchingPredicateOrAfterXStanzas(PacketFilter predicate, int count) { + this.predicate = predicate; + this.afterXStanzas = new AfterXStanzas(count); + } + + @Override + public boolean accept(Packet packet) { + if (predicate.accept(packet)) { + afterXStanzas.resetCounter(); + return true; + } + return afterXStanzas.accept(packet); + } +} diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/sm/predicates/OnceForThisStanza.java b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/sm/predicates/OnceForThisStanza.java new file mode 100644 index 000000000..2c4689205 --- /dev/null +++ b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/sm/predicates/OnceForThisStanza.java @@ -0,0 +1,55 @@ +/** + * + * Copyright © 2014 Florian Schmaus + * + * 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.tcp.sm.predicates; + +import org.jivesoftware.smack.filter.PacketFilter; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.tcp.XMPPTCPConnection; +import org.jivesoftware.smack.util.StringUtils; + +public class OnceForThisStanza implements PacketFilter { + + private final String id; + private final XMPPTCPConnection connection; + + public static void setup(XMPPTCPConnection connection, Packet packet) { + PacketFilter packetFilter = new OnceForThisStanza(connection, packet); + connection.addRequestAckPredicate(packetFilter); + } + + private OnceForThisStanza(XMPPTCPConnection connection, Packet packet) { + this.connection = connection; + this.id = packet.getPacketID(); + if (StringUtils.isNullOrEmpty(id)) { + throw new IllegalArgumentException("Stanza ID must be set"); + } + } + + @Override + public boolean accept(Packet packet) { + String otherId = packet.getPacketID(); + if (StringUtils.isNullOrEmpty(otherId)) { + return false; + } + if (id.equals(otherId)) { + connection.removeRequestAckPredicate(this); + return true; + } + return false; + } + +} diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/sm/predicates/Predicate.java b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/sm/predicates/Predicate.java new file mode 100644 index 000000000..d70db568b --- /dev/null +++ b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/sm/predicates/Predicate.java @@ -0,0 +1,30 @@ +/** + * + * Copyright © 2014 Florian Schmaus + * + * 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.tcp.sm.predicates; + +import org.jivesoftware.smack.filter.PacketFilter; + +public class Predicate { + + public static PacketFilter forMessagesOrAfter5Stanzas() { + return new ForMatchingPredicateOrAfterXStanzas(ForEveryMessage.INSTANCE, 5); + } + + public static AfterXStanzas after5Stanzas() { + return new AfterXStanzas(5); + } +} diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/sm/predicates/ShortcutPredicates.java b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/sm/predicates/ShortcutPredicates.java new file mode 100644 index 000000000..96e422133 --- /dev/null +++ b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/sm/predicates/ShortcutPredicates.java @@ -0,0 +1,54 @@ +/** + * + * Copyright © 2014 Florian Schmaus + * + * 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.tcp.sm.predicates; + +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.jivesoftware.smack.filter.PacketFilter; +import org.jivesoftware.smack.packet.Packet; + +public class ShortcutPredicates implements PacketFilter { + + private final Set predicates = new LinkedHashSet(); + + public ShortcutPredicates() { + } + + public ShortcutPredicates(Collection predicates) { + this.predicates.addAll(predicates); + } + + public boolean addPredicate(PacketFilter predicate) { + return predicates.add(predicate); + } + + public boolean removePredicate(PacketFilter prediacte) { + return predicates.remove(prediacte); + } + + @Override + public boolean accept(Packet packet) { + for (PacketFilter predicate : predicates) { + if (predicate.accept(packet)) { + return true; + } + } + return false; + } +} diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/sm/provider/ParseStreamManagement.java b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/sm/provider/ParseStreamManagement.java new file mode 100644 index 000000000..6d3168885 --- /dev/null +++ b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/sm/provider/ParseStreamManagement.java @@ -0,0 +1,88 @@ +/** + * + * Copyright © 2014 Florian Schmaus + * + * 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.tcp.sm.provider; + +import java.io.IOException; + +import org.jivesoftware.smack.packet.XMPPError; +import org.jivesoftware.smack.tcp.sm.packet.StreamManagement.AckAnswer; +import org.jivesoftware.smack.tcp.sm.packet.StreamManagement.Enabled; +import org.jivesoftware.smack.tcp.sm.packet.StreamManagement.Failed; +import org.jivesoftware.smack.tcp.sm.packet.StreamManagement.Resumed; +import org.jivesoftware.smack.util.ParserUtils; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +public class ParseStreamManagement { + + public static Enabled enabled(XmlPullParser parser) throws XmlPullParserException, IOException { + ParserUtils.assertAtStartTag(parser); + boolean resume = ParserUtils.getBooleanAttribute(parser, "resume", false); + String id = parser.getAttributeValue("", "id"); + String location = parser.getAttributeValue("", "location"); + int max = ParserUtils.getIntegerAttribute(parser, "max", -1); + parser.next(); + ParserUtils.assertAtEndTag(parser); + return new Enabled(id, resume, location, max); + } + + public static Failed failed(XmlPullParser parser) throws XmlPullParserException, IOException { + ParserUtils.assertAtStartTag(parser); + String name; + String condition = "unknown"; + outerloop: + while(true) { + int event = parser.next(); + switch (event) { + case XmlPullParser.START_TAG: + name = parser.getName(); + String namespace = parser.getNamespace(); + if (XMPPError.NAMESPACE.equals(namespace)) { + condition = name; + } + break; + case XmlPullParser.END_TAG: + name = parser.getName(); + if (Failed.ELEMENT.equals(name)) { + break outerloop; + } + break; + } + } + ParserUtils.assertAtEndTag(parser); + XMPPError error = new XMPPError(condition); + return new Failed(error); + } + + public static Resumed resumed(XmlPullParser parser) throws XmlPullParserException, IOException { + ParserUtils.assertAtStartTag(parser); + long h = ParserUtils.getLongAttribute(parser, "h"); + String previd = parser.getAttributeValue("", "previd"); + parser.next(); + ParserUtils.assertAtEndTag(parser); + return new Resumed(h, previd); + } + + public static AckAnswer ackAnswer(XmlPullParser parser) throws XmlPullParserException, IOException { + ParserUtils.assertAtStartTag(parser); + long h = ParserUtils.getLongAttribute(parser, "h"); + parser.next(); + ParserUtils.assertAtEndTag(parser); + return new AckAnswer(h); + } + +} diff --git a/smack-tcp/src/test/java/org/jivesoftware/smack/tcp/PacketWriterTest.java b/smack-tcp/src/test/java/org/jivesoftware/smack/tcp/PacketWriterTest.java index 8cc428f61..f7e56611a 100644 --- a/smack-tcp/src/test/java/org/jivesoftware/smack/tcp/PacketWriterTest.java +++ b/smack-tcp/src/test/java/org/jivesoftware/smack/tcp/PacketWriterTest.java @@ -40,19 +40,22 @@ public class PacketWriterTest { * * @throws InterruptedException * @throws BrokenBarrierException + * @throws NotConnectedException */ @SuppressWarnings("javadoc") @Test public void shouldBlockAndUnblockTest() throws InterruptedException, BrokenBarrierException, NotConnectedException { XMPPTCPConnection connection = new XMPPTCPConnection("foobar.com"); final PacketWriter pw = connection.new PacketWriter(); - pw.setWriter(new BlockingStringWriter()); - pw.startup(); + connection.packetWriter = pw; + connection.packetReader = connection.new PacketReader(); + connection.setWriter(new BlockingStringWriter()); + pw.init(); for (int i = 0; i < XMPPTCPConnection.PacketWriter.QUEUE_SIZE; i++) { - pw.sendPacket(new Message()); + pw.sendStreamElement(new Message()); } - + final CyclicBarrier barrier = new CyclicBarrier(2); shutdown = false; prematureUnblocked = false; @@ -61,7 +64,7 @@ public class PacketWriterTest { public void run() { try { barrier.await(); - pw.sendPacket(new Message()); + pw.sendStreamElement(new Message()); // should only return after the pw was interrupted if (!shutdown) { prematureUnblocked = true; @@ -85,9 +88,9 @@ public class PacketWriterTest { Thread.sleep(250); // Set to true for testing purposes, so that shutdown() won't wait packet writer - pw.shutdownDone.set(true); + pw.shutdownDone.reportSuccess(); // Shutdown the packetwriter - pw.shutdown(); + pw.shutdown(false); shutdown = true; barrier.await(); if (prematureUnblocked) { diff --git a/smack-tcp/src/test/java/org/jivesoftware/smack/tcp/sm/provider/ParseStreamManagementTest.java b/smack-tcp/src/test/java/org/jivesoftware/smack/tcp/sm/provider/ParseStreamManagementTest.java new file mode 100644 index 000000000..eaccfbdbf --- /dev/null +++ b/smack-tcp/src/test/java/org/jivesoftware/smack/tcp/sm/provider/ParseStreamManagementTest.java @@ -0,0 +1,154 @@ +/** + * + * Copyright 2014 Vyacheslav Blinov + * + * 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.tcp.sm.provider; + +import com.jamesmurty.utils.XMLBuilder; +import org.jivesoftware.smack.packet.XMPPError; +import org.jivesoftware.smack.tcp.sm.packet.StreamManagement; +import org.jivesoftware.smack.util.PacketParserUtils; +import org.junit.Test; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.util.Properties; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; + +public class ParseStreamManagementTest { + private static final Properties outputProperties = initOutputProperties(); + + @Test + public void testParseEnabled() throws Exception { + String stanzaID = "zid615d9"; + boolean resume = true; + String location = "test"; + int max = 42; + + String enabledStanza = XMLBuilder.create("enabled") + .a("xmlns", "urn:xmpp:sm:3") + .a("id", "zid615d9") + .a("resume", String.valueOf(resume)) + .a("location", location) + .a("max", String.valueOf(max)) + .asString(outputProperties); + + StreamManagement.Enabled enabledPacket = ParseStreamManagement.enabled( + PacketParserUtils.getParserFor(enabledStanza)); + + assertThat(enabledPacket, is(notNullValue())); + assertThat(enabledPacket.getId(), equalTo(stanzaID)); + assertThat(enabledPacket.getLocation(), equalTo(location)); + assertThat(enabledPacket.isResumeSet(), equalTo(resume)); + assertThat(enabledPacket.getMaxResumptionTime(), equalTo(max)); + } + + + @Test + public void testParseEnabledInvariant() throws XmlPullParserException, IOException { + String enabledString = (new StreamManagement.Enabled("stream-id", false)).toXML().toString(); + XmlPullParser parser = PacketParserUtils.getParserFor(enabledString); + StreamManagement.Enabled enabled = ParseStreamManagement.enabled(parser); + + assertEquals(enabledString, enabled.toXML().toString()); + } + + @Test + public void testParseFailed() throws Exception { + String failedStanza = XMLBuilder.create("failed") + .a("xmlns", "urn:xmpp:sm:3") + .asString(outputProperties); + + StreamManagement.Failed failedPacket = ParseStreamManagement.failed( + PacketParserUtils.getParserFor(failedStanza)); + + assertThat(failedPacket, is(notNullValue())); + XMPPError error = failedPacket.getXMPPError(); + + assertThat(error, is(notNullValue())); + assertThat(error.getCondition(), equalTo("unknown")); + } + + @Test + public void testParseFailedError() throws Exception { + String errorCondition = "failure"; + + String failedStanza = XMLBuilder.create("failed") + .a("xmlns", "urn:xmpp:sm:3") + .element(errorCondition, XMPPError.NAMESPACE) + .asString(outputProperties); + + System.err.println(failedStanza); + + StreamManagement.Failed failedPacket = ParseStreamManagement.failed( + PacketParserUtils.getParserFor(failedStanza)); + + assertThat(failedPacket, is(notNullValue())); + XMPPError error = failedPacket.getXMPPError(); + + assertThat(error, is(notNullValue())); + assertThat(error.getCondition(), equalTo(errorCondition)); + } + + @Test + public void testParseResumed() throws Exception { + long handledPackets = 42; + String previousID = "zid615d9"; + + String resumedStanza = XMLBuilder.create("resumed") + .a("xmlns", "urn:xmpp:sm:3") + .a("h", String.valueOf(handledPackets)) + .a("previd", previousID) + .asString(outputProperties); + + StreamManagement.Resumed resumedPacket = ParseStreamManagement.resumed( + PacketParserUtils.getParserFor(resumedStanza)); + + assertThat(resumedPacket, is(notNullValue())); + assertThat(resumedPacket.getHandledCount(), equalTo(handledPackets)); + assertThat(resumedPacket.getPrevId(), equalTo(previousID)); + } + + @Test + public void testParseAckAnswer() throws Exception { + long handledPackets = 42 + 42; + + String ackStanza = XMLBuilder.create("a") + .a("xmlns", "urn:xmpp:sm:3") + .a("h", String.valueOf(handledPackets)) + .asString(outputProperties); + + StreamManagement.AckAnswer acknowledgementPacket = ParseStreamManagement.ackAnswer( + PacketParserUtils.getParserFor(ackStanza)); + + assertThat(acknowledgementPacket, is(notNullValue())); + assertThat(acknowledgementPacket.getHandledCount(), equalTo(handledPackets)); + } + + + private static Properties initOutputProperties() { + Properties properties = new Properties(); + properties.put(javax.xml.transform.OutputKeys.OMIT_XML_DECLARATION, "yes"); + return properties; + } +}