diff --git a/resources/releasedocs/changelog.html b/resources/releasedocs/changelog.html index 5d249c249..706a4e2c6 100644 --- a/resources/releasedocs/changelog.html +++ b/resources/releasedocs/changelog.html @@ -141,6 +141,24 @@ hr {
+

4.3.3 -- 2019-03-14

+ +

Bug +

+ + +

Improvement +

+ +

4.3.2 -- 2019-02-22

Bug 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 1dbaf91cb..4270ffdee 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 @@ -266,6 +266,11 @@ public class XMPPBOSHConnection extends AbstractXMPPConnection { client = null; } + instantShutdown(); + } + + @Override + public void instantShutdown() { setWasAuthenticated(); sessionID = null; done = true; 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 dfe4d5662..53236e6b1 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/AbstractXMPPConnection.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/AbstractXMPPConnection.java @@ -111,6 +111,7 @@ import org.jivesoftware.smack.provider.ExtensionElementProvider; import org.jivesoftware.smack.provider.NonzaProvider; import org.jivesoftware.smack.provider.ProviderManager; import org.jivesoftware.smack.sasl.core.SASLAnonymous; +import org.jivesoftware.smack.util.Async; import org.jivesoftware.smack.util.DNSUtil; import org.jivesoftware.smack.util.Objects; import org.jivesoftware.smack.util.PacketParserUtils; @@ -357,6 +358,9 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { */ protected boolean authenticated = false; + // TODO: Migrate to ZonedDateTime once Smack's minimum required Android SDK level is 26 (8.0, Oreo) or higher. + protected long authenticatedConnectionInitiallyEstablishedTimestamp; + /** * Flag that indicates if the user was authenticated with the server when the connection * to the server was closed (abruptly or not). @@ -449,6 +453,12 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { @Override public abstract boolean isUsingCompression(); + protected void initState() { + saslFeatureReceived.init(); + lastFeaturesReceived.init(); + tlsHandled.init(); + } + /** * Establishes a connection to the XMPP server. It basically * creates and maintains a connection to the server. @@ -467,21 +477,23 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { throwAlreadyConnectedExceptionIfAppropriate(); // Reset the connection state + initState(); saslAuthentication.init(); - saslFeatureReceived.init(); - lastFeaturesReceived.init(); - tlsHandled.init(); streamId = null; - // Perform the actual connection to the XMPP service - connectInternal(); + try { + // Perform the actual connection to the XMPP service + connectInternal(); - // 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() && getConfiguration().getSecurityMode() == SecurityMode.required) { - shutdown(); - throw new SecurityRequiredByClientException(); + // 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() && getConfiguration().getSecurityMode() == SecurityMode.required) { + throw new SecurityRequiredByClientException(); + } + } catch (SmackException | IOException | XMPPException | InterruptedException e) { + instantShutdown(); + throw e; } // Make note of the fact that we're now connected. @@ -652,6 +664,9 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { } protected void afterSuccessfulLogin(final boolean resumed) throws NotConnectedException, InterruptedException { + if (!resumed) { + authenticatedConnectionInitiallyEstablishedTimestamp = System.currentTimeMillis(); + } // Indicate that we're now authenticated. this.authenticated = true; @@ -834,36 +849,46 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { * * @param exception the exception that causes the connection close event. */ - protected final synchronized void notifyConnectionError(Exception exception) { + protected final void notifyConnectionError(final Exception exception) { if (!isConnected()) { LOGGER.log(Level.INFO, "Connection was already disconnected when attempting to handle " + exception, exception); return; } - currentConnectionException = exception; - notifyAll(); + ASYNC_BUT_ORDERED.performAsyncButOrdered(this, () -> { + currentConnectionException = exception; + synchronized (AbstractXMPPConnection.this) { + notifyAll(); + } - for (StanzaCollector collector : collectors) { - collector.notifyConnectionError(exception); - } - SmackWrappedException smackWrappedException = new SmackWrappedException(exception); - tlsHandled.reportGenericFailure(smackWrappedException); - saslFeatureReceived.reportGenericFailure(smackWrappedException); - lastFeaturesReceived.reportGenericFailure(smackWrappedException); + for (StanzaCollector collector : collectors) { + collector.notifyConnectionError(exception); + } + SmackWrappedException smackWrappedException = new SmackWrappedException(exception); + tlsHandled.reportGenericFailure(smackWrappedException); + saslFeatureReceived.reportGenericFailure(smackWrappedException); + lastFeaturesReceived.reportGenericFailure(smackWrappedException); + // TODO From XMPPTCPConnection. Was called in Smack 4.3 where notifyConnectionError() was part of + // XMPPTCPConnection. Create delegation method? + // maybeCompressFeaturesReceived.reportGenericFailure(smackWrappedException); - // Closes the connection temporary. A if the connection supports stream management, then a reconnection is - // possible. Note that a connection listener of e.g. XMPPTCPConnection will drop the SM state in - // case the Exception is a StreamErrorException. - instantShutdown(); + // Closes the connection temporary. A if the connection supports stream management, then a reconnection is + // possible. Note that a connection listener of e.g. XMPPTCPConnection will drop the SM state in + // case the Exception is a StreamErrorException. + instantShutdown(); - callConnectionClosedOnErrorListener(exception); + Async.go(() -> { + // Notify connection listeners of the error. + callConnectionClosedOnErrorListener(exception); + }, AbstractXMPPConnection.this + " callConnectionClosedOnErrorListener()"); + }); } - protected void instantShutdown() { - // Default implementation simply calls shutdown(), subclasses may override this. - shutdown(); - } + /** + * Performs an unclean disconnect and shutdown of the connection. Does not send a closing stream stanza. + */ + public abstract void instantShutdown(); /** * Shuts the current connection down. @@ -1811,6 +1836,18 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { return lastStanzaReceived; } + /** + * Get the timestamp when the connection was the first time authenticated, i.e., when the first successful login was + * performed. Note that this value is not reset on disconnect, so it represents the timestamp from the last + * authenticated connection. The value is also not reset on stream resumption. + * + * @return the timestamp or {@code null}. + * @since 4.3.3 + */ + public final long getAuthenticatedConnectionInitiallyEstablishedTimestamp() { + return authenticatedConnectionInitiallyEstablishedTimestamp; + } + /** * Install a parsing exception callback, which will be invoked once an exception is encountered while parsing a * stanza. 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 5c1328b1a..41b3b417f 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/SmackException.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/SmackException.java @@ -93,17 +93,24 @@ public abstract class SmackException extends Exception { return new NoResponseException(sb.toString()); } - public static NoResponseException newWith(XMPPConnection connection, + @Deprecated + // TODO: Remove in Smack 4.4. + public static NoResponseException newWith(long timeout, + StanzaCollector collector) { + return newWith(timeout, collector.getStanzaFilter(), false); + } + + public static NoResponseException newWith(long timeout, StanzaCollector collector, boolean stanzaCollectorCancelled) { - return newWith(connection, collector.getStanzaFilter(), stanzaCollectorCancelled); + return newWith(timeout, collector.getStanzaFilter(), stanzaCollectorCancelled); } public static NoResponseException newWith(XMPPConnection connection, StanzaFilter filter) { - return newWith(connection, filter, false); + return newWith(connection.getReplyTimeout(), filter, false); } - public static NoResponseException newWith(XMPPConnection connection, StanzaFilter filter, boolean stanzaCollectorCancelled) { - final StringBuilder sb = getWaitingFor(connection); + public static NoResponseException newWith(long timeout, StanzaFilter filter, boolean stanzaCollectorCancelled) { + final StringBuilder sb = getWaitingFor(timeout); if (stanzaCollectorCancelled) { sb.append(" StanzaCollector has been cancelled."); } @@ -119,7 +126,10 @@ public abstract class SmackException extends Exception { } private static StringBuilder getWaitingFor(XMPPConnection connection) { - final long replyTimeout = connection.getReplyTimeout(); + return getWaitingFor(connection.getReplyTimeout()); + } + + private static StringBuilder getWaitingFor(final long replyTimeout) { final StringBuilder sb = new StringBuilder(256); sb.append("No response received within reply timeout. Timeout was " + replyTimeout + "ms (~" diff --git a/smack-core/src/main/java/org/jivesoftware/smack/StanzaCollector.java b/smack-core/src/main/java/org/jivesoftware/smack/StanzaCollector.java index 800c84406..9d3eeeda8 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/StanzaCollector.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/StanzaCollector.java @@ -290,7 +290,7 @@ public class StanzaCollector implements AutoCloseable { if (!connection.isConnected()) { throw new NotConnectedException(connection, packetFilter); } - throw NoResponseException.newWith(connection, this, cancelled); + throw NoResponseException.newWith(timeout, this, cancelled); } XMPPErrorException.ifHasErrorThenThrow(result); diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/StanzaError.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/StanzaError.java index f6cd5f4c6..a5b89c031 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/packet/StanzaError.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/StanzaError.java @@ -198,6 +198,12 @@ public class StanzaError extends AbstractError implements ExtensionElement { public String toString() { StringBuilder sb = new StringBuilder("XMPPError: "); sb.append(condition.toString()).append(" - ").append(type.toString()); + + String descriptiveText = getDescriptiveText(); + if (descriptiveText != null) { + sb.append(" [").append(descriptiveText).append(']'); + } + if (errorGenerator != null) { sb.append(". Generated by ").append(errorGenerator); } diff --git a/smack-core/src/test/java/org/jivesoftware/smack/DummyConnection.java b/smack-core/src/test/java/org/jivesoftware/smack/DummyConnection.java index bfe286823..81c1f9ead 100644 --- a/smack-core/src/test/java/org/jivesoftware/smack/DummyConnection.java +++ b/smack-core/src/test/java/org/jivesoftware/smack/DummyConnection.java @@ -100,6 +100,11 @@ public class DummyConnection extends AbstractXMPPConnection { callConnectionClosedListener(); } + @Override + public void instantShutdown() { + shutdown(); + } + @Override public boolean isSecureConnection() { return false; @@ -226,4 +231,5 @@ public class DummyConnection extends AbstractXMPPConnection { } } } + } diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/sid/StableUniqueStanzaIdManager.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/sid/StableUniqueStanzaIdManager.java index 677a38910..99ec03f5d 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/sid/StableUniqueStanzaIdManager.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/sid/StableUniqueStanzaIdManager.java @@ -27,7 +27,6 @@ import org.jivesoftware.smack.XMPPConnectionRegistry; import org.jivesoftware.smack.filter.AndFilter; import org.jivesoftware.smack.filter.MessageTypeFilter; import org.jivesoftware.smack.filter.NotFilter; -import org.jivesoftware.smack.filter.StanzaExtensionFilter; import org.jivesoftware.smack.filter.StanzaFilter; import org.jivesoftware.smack.filter.ToTypeFilter; import org.jivesoftware.smack.packet.Message; @@ -46,13 +45,12 @@ public final class StableUniqueStanzaIdManager extends Manager { MessageTypeFilter.NORMAL_OR_CHAT_OR_HEADLINE, ToTypeFilter.ENTITY_FULL_OR_BARE_JID); - private static final StanzaFilter ORIGIN_ID_FILTER = new StanzaExtensionFilter(OriginIdElement.ELEMENT, NAMESPACE); - // Listener for outgoing stanzas that adds origin-ids to outgoing stanzas. - private final StanzaListener stanzaListener = new StanzaListener() { + private static final StanzaListener ADD_ORIGIN_ID_INTERCEPTOR = new StanzaListener() { @Override public void processStanza(Stanza stanza) { - OriginIdElement.addOriginId((Message) stanza); + Message message = (Message) stanza; + OriginIdElement.addOriginId(message); } }; @@ -95,7 +93,7 @@ public final class StableUniqueStanzaIdManager extends Manager { public synchronized void enable() { ServiceDiscoveryManager.getInstanceFor(connection()).addFeature(NAMESPACE); StanzaFilter filter = new AndFilter(OUTGOING_FILTER, new NotFilter(OUTGOING_FILTER)); - connection().addStanzaInterceptor(stanzaListener, filter); + connection().addStanzaInterceptor(ADD_ORIGIN_ID_INTERCEPTOR, filter); } /** @@ -103,7 +101,7 @@ public final class StableUniqueStanzaIdManager extends Manager { */ public synchronized void disable() { ServiceDiscoveryManager.getInstanceFor(connection()).removeFeature(NAMESPACE); - connection().removeStanzaInterceptor(stanzaListener); + connection().removeStanzaInterceptor(ADD_ORIGIN_ID_INTERCEPTOR); } /** diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/sid/provider/OriginIdProvider.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/sid/provider/OriginIdProvider.java index e2a4447d0..56a92e8b9 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/sid/provider/OriginIdProvider.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/sid/provider/OriginIdProvider.java @@ -24,7 +24,11 @@ import org.xmlpull.v1.XmlPullParser; public class OriginIdProvider extends ExtensionElementProvider { - public static final OriginIdProvider TEST_INSTANCE = new OriginIdProvider(); + public static final OriginIdProvider INSTANCE = new OriginIdProvider(); + + // TODO: Remove in Smack 4.4. + @Deprecated + public static final OriginIdProvider TEST_INSTANCE = INSTANCE; @Override public OriginIdElement parse(XmlPullParser parser, int initialDepth, XmlEnvironment xmlEnvironment) { diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/sid/provider/StanzaIdProvider.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/sid/provider/StanzaIdProvider.java index 8d9ce305c..6f336b55c 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/sid/provider/StanzaIdProvider.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/sid/provider/StanzaIdProvider.java @@ -24,7 +24,11 @@ import org.xmlpull.v1.XmlPullParser; public class StanzaIdProvider extends ExtensionElementProvider { - public static StanzaIdProvider TEST_INSTANCE = new StanzaIdProvider(); + public static final StanzaIdProvider INSTANCE = new StanzaIdProvider(); + + // TODO: Remove in Smack 4.4. + @Deprecated + public static final StanzaIdProvider TEST_INSTANCE = INSTANCE; @Override public StanzaIdElement parse(XmlPullParser parser, int initialDepth, XmlEnvironment xmlEnvironment) { diff --git a/smack-experimental/src/test/java/org/jivesoftware/smackx/sid/StableUniqueStanzaIdTest.java b/smack-experimental/src/test/java/org/jivesoftware/smackx/sid/StableUniqueStanzaIdTest.java index cf8ca2f33..5e65ba408 100644 --- a/smack-experimental/src/test/java/org/jivesoftware/smackx/sid/StableUniqueStanzaIdTest.java +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/sid/StableUniqueStanzaIdTest.java @@ -25,6 +25,7 @@ import static org.custommonkey.xmlunit.XMLAssert.assertXMLEqual; import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.test.util.SmackTestSuite; import org.jivesoftware.smack.test.util.TestUtils; +import org.jivesoftware.smack.util.PacketParserUtils; import org.jivesoftware.smackx.sid.element.OriginIdElement; import org.jivesoftware.smackx.sid.element.StanzaIdElement; import org.jivesoftware.smackx.sid.provider.OriginIdProvider; @@ -42,7 +43,7 @@ public class StableUniqueStanzaIdTest extends SmackTestSuite { assertEquals("alice@wonderland.lit", element.getBy()); assertXMLEqual(xml, element.toXML().toString()); - StanzaIdElement parsed = StanzaIdProvider.TEST_INSTANCE.parse(TestUtils.getParser(xml)); + StanzaIdElement parsed = StanzaIdProvider.INSTANCE.parse(TestUtils.getParser(xml)); assertEquals(element.getId(), parsed.getId()); assertEquals(element.getBy(), parsed.getBy()); } @@ -54,7 +55,7 @@ public class StableUniqueStanzaIdTest extends SmackTestSuite { assertEquals("de305d54-75b4-431b-adb2-eb6b9e546013", element.getId()); assertXMLEqual(xml, element.toXML().toString()); - OriginIdElement parsed = OriginIdProvider.TEST_INSTANCE.parse(TestUtils.getParser(xml)); + OriginIdElement parsed = OriginIdProvider.INSTANCE.parse(TestUtils.getParser(xml)); assertEquals(element.getId(), parsed.getId()); } @@ -81,4 +82,17 @@ public class StableUniqueStanzaIdTest extends SmackTestSuite { assertTrue(StanzaIdElement.hasStanzaId(message)); assertEquals(stanzaId, StanzaIdElement.getStanzaId(message)); } + + @Test + public void testMultipleUssidExtensions() throws Exception { + String message = "" + + "Test message" + + "" + + "" + + "" + + ""; + Message messageStanza = PacketParserUtils.parseStanza(message); + + assertTrue(StanzaIdElement.hasStanzaId(messageStanza)); + } } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChat.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChat.java index 0ccf9ba24..fd6457964 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChat.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChat.java @@ -341,9 +341,10 @@ public class MultiUserChat { // Setup the messageListeners and presenceListeners *before* the join presence is send. connection.addSyncStanzaListener(messageListener, fromRoomGroupchatFilter); - connection.addSyncStanzaListener(presenceListener, new AndFilter(fromRoomFilter, + StanzaFilter presenceFromRoomFilter = new AndFilter(fromRoomFilter, StanzaTypeFilter.PRESENCE, - PossibleFromTypeFilter.ENTITY_FULL_JID)); + PossibleFromTypeFilter.ENTITY_FULL_JID); + connection.addSyncStanzaListener(presenceListener, presenceFromRoomFilter); // @formatter:off connection.addSyncStanzaListener(subjectListener, new AndFilter(fromRoomFilter, @@ -372,15 +373,27 @@ public class MultiUserChat { ) ); // @formatter:on + StanzaCollector presenceStanzaCollector = null; Presence presence; try { - presence = connection.createStanzaCollectorAndSend(responseFilter, joinPresence).nextResultOrThrow(conf.getTimeout()); + // This stanza collector will collect the final self presence from the MUC, which also signals that we have successful entered the MUC. + StanzaCollector selfPresenceCollector = connection.createStanzaCollectorAndSend(responseFilter, joinPresence); + StanzaCollector.Configuration presenceStanzaCollectorConfguration = StanzaCollector.newConfiguration().setCollectorToReset( + selfPresenceCollector).setStanzaFilter(presenceFromRoomFilter); + // This stanza collector is used to reset the timeout of the selfPresenceCollector. + presenceStanzaCollector = connection.createStanzaCollector(presenceStanzaCollectorConfguration); + presence = selfPresenceCollector.nextResultOrThrow(conf.getTimeout()); } catch (NotConnectedException | InterruptedException | NoResponseException | XMPPErrorException e) { // Ensure that all callbacks are removed if there is an exception removeConnectionCallbacks(); throw e; } + finally { + if (presenceStanzaCollector != null) { + presenceStanzaCollector.cancel(); + } + } // This presence must be send from a full JID. We use the resourcepart of this JID as nick, since the room may // performed roomnick rewriting diff --git a/smack-resolver-javax/src/main/java/org/jivesoftware/smack/util/dns/javax/JavaxResolver.java b/smack-resolver-javax/src/main/java/org/jivesoftware/smack/util/dns/javax/JavaxResolver.java index e83c588f6..957d8006c 100644 --- a/smack-resolver-javax/src/main/java/org/jivesoftware/smack/util/dns/javax/JavaxResolver.java +++ b/smack-resolver-javax/src/main/java/org/jivesoftware/smack/util/dns/javax/JavaxResolver.java @@ -18,6 +18,7 @@ package org.jivesoftware.smack.util.dns.javax; import java.net.InetAddress; import java.util.ArrayList; +import java.util.Hashtable; import java.util.List; import java.util.logging.Level; @@ -40,6 +41,7 @@ import org.minidns.dnsname.DnsName; /** * A DNS resolver (mostly for SRV records), which makes use of the API provided in the javax.* namespace. + * Note that using JavaxResovler requires applications using newer Java versions (at least 11) to declare a dependency on the "sun.jdk" module. * * @author Florian Schmaus * @@ -51,7 +53,9 @@ public class JavaxResolver extends DNSResolver implements SmackInitializer { static { try { - dirContext = new InitialDirContext(); + Hashtable env = new Hashtable<>(); + env.put("java.naming.factory.initial", "com.sun.jndi.dns.DnsContextFactory"); + dirContext = new InitialDirContext(env); } catch (NamingException e) { LOGGER.log(Level.SEVERE, "Could not construct InitialDirContext", e); } 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 39e658601..68b1fe8a0 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 @@ -257,6 +257,15 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { */ private final Collection stanzaAcknowledgedListeners = new ConcurrentLinkedQueue<>(); + /** + * These listeners are invoked for every stanza that got dropped. + *

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

+ */ + private final Collection stanzaDroppedListeners = 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. @@ -421,9 +430,24 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { } } } - // (Re-)send the stanzas *after* we tried to enable SM - for (Stanza stanza : previouslyUnackedStanzas) { - sendStanzaInternal(stanza); + // Inform client about failed resumption if possible, resend stanzas otherwise + // Process the stanzas synchronously so a client can re-queue them for transmission + // before it is informed about connection success + if (!stanzaDroppedListeners.isEmpty()) { + for (Stanza stanza : previouslyUnackedStanzas) { + for (StanzaListener listener : stanzaDroppedListeners) { + try { + listener.processStanza(stanza); + } + catch (InterruptedException | NotConnectedException | NotLoggedInException e) { + LOGGER.log(Level.FINER, "StanzaDroppedListener received exception", e); + } + } + } + } else { + for (Stanza stanza : previouslyUnackedStanzas) { + sendStanzaInternal(stanza); + } } afterSuccessfulLogin(false); @@ -452,9 +476,6 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { shutdown(false); } - /** - * Performs an unclean disconnect and shutdown of the connection. Does not send a closing stream stanza. - */ @Override public synchronized void instantShutdown() { shutdown(true); @@ -492,6 +513,8 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { // Reset the stream management session id to null, since if the stream is cleanly closed, i.e. sending a closing // stream tag, there is no longer a stream to resume. smSessionId = null; + // Note that we deliberately do not reset authenticatedConnectionInitiallyEstablishedTimestamp here, so that the + // information is available in the connectionClosedOnError() listeners. } authenticated = false; connected = false; @@ -499,6 +522,16 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { reader = null; writer = null; + initState(); + + // Wait for reader and writer threads to be terminated. + readerWriterSemaphore.acquireUninterruptibly(2); + readerWriterSemaphore.release(2); + } + + @Override + protected void initState() { + super.initState(); maybeCompressFeaturesReceived.init(); compressSyncPoint.init(); smResumedSyncPoint.init(); @@ -784,14 +817,13 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { } @Override - protected void afterFeaturesReceived() throws NotConnectedException, InterruptedException { + protected void afterFeaturesReceived() throws NotConnectedException, InterruptedException, SecurityRequiredByServerException { StartTls startTlsFeature = getFeature(StartTls.ELEMENT, StartTls.NAMESPACE); if (startTlsFeature != null) { if (startTlsFeature.required() && config.getSecurityMode() == SecurityMode.disabled) { - SmackException smackException = new SecurityRequiredByServerException(); + SecurityRequiredByServerException smackException = new SecurityRequiredByServerException(); tlsHandled.reportFailure(smackException); - notifyConnectionError(smackException); - return; + throw smackException; } if (config.getSecurityMode() != ConnectionConfiguration.SecurityMode.disabled) { @@ -1062,7 +1094,11 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { LOGGER.info(XMPPTCPConnection.this + " received closing element." + " Server wants to terminate the connection, calling disconnect()"); - disconnect(); + ASYNC_BUT_ORDERED.performAsyncButOrdered(XMPPTCPConnection.this, new Runnable() { + @Override + public void run() { + disconnect(); + }}); } } break; @@ -1567,6 +1603,32 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { stanzaAcknowledgedListeners.clear(); } + /** + * Add a Stanza dropped listener. + *

+ * Those listeners will be invoked every time a Stanza has been dropped due to a failed SM resume. They will not get + * automatically removed. If at least one StanzaDroppedListener is configured, no attempt will be made to retransmit + * the Stanzas. + *

+ * + * @param listener the listener to add. + * @since 4.3.3 + */ + public void addStanzaDroppedListener(StanzaListener listener) { + stanzaDroppedListeners.add(listener); + } + + /** + * Remove the given Stanza dropped listener. + * + * @param listener the listener. + * @return true if the listener was removed. + * @since 4.3.3 + */ + public boolean removeStanzaDroppedListener(StanzaListener listener) { + return stanzaDroppedListeners.remove(listener); + } + /** * Add a new Stanza ID acknowledged listener for the given ID. *

diff --git a/version.gradle b/version.gradle index f6e8cc09f..28e90a9a4 100644 --- a/version.gradle +++ b/version.gradle @@ -2,6 +2,12 @@ allprojects { ext { shortVersion = '4.4.0-alpha2' isSnapshot = true + // When using dynamic versions for those, do *not* use [1.0, + // 2.0), since this will also pull in 2.0-alpha1. Instead use + // [1.0, 1.0.99]. + // See also: + // - https://issues.apache.org/jira/browse/MNG-6232 + // - https://issues.igniterealtime.org/browse/SMACK-858 jxmppVersion = '0.7.0-alpha5' miniDnsVersion = '0.4.0-alpha3' smackMinAndroidSdk = 19