diff --git a/config/checkstyle.xml b/config/checkstyle.xml index a05c12726..d925f4909 100644 --- a/config/checkstyle.xml +++ b/config/checkstyle.xml @@ -6,6 +6,11 @@ + + + + + @@ -61,6 +66,14 @@ + + + + + @@ -89,6 +102,11 @@ + + + + + diff --git a/resources/releasedocs/changelog.html b/resources/releasedocs/changelog.html index 4d51e4594..706a4e2c6 100644 --- a/resources/releasedocs/changelog.html +++ b/resources/releasedocs/changelog.html @@ -141,6 +141,74 @@ hr {
+

4.3.3 -- 2019-03-14

+ +

Bug +

+
    +
  • [SMACK-856] - Smack fails under JDK 11 because com.sun.jndi.dns.DnsContextFactory is not inaccessible +
  • +
+ +

Improvement +

+
    +
  • [SMACK-858] - Dependency version specifier of jxmpp and MiniDNS include alpha/beta/... versions of the follow up version when Maven is used +
  • +
  • [SMACK-859] - MultiUserChat enter() should reset the timeout of the collector waiting for the final self presence to prevent timeouts for large MUCs +
  • +
+ +

4.3.2 -- 2019-02-22

+ +

Bug +

+
    +
  • [SMACK-842] - The RFC 3920 xml-not-well-formed error condition should be handled in stream error not a stanza error +
  • +
  • [SMACK-843] - ManManager.pagePrevious() pages into the wrong direction +
  • +
  • [SMACK-844] - Check if bounded unacknowledged stanzas queue is full before adding to it to avoid IllegalStateException +
  • +
  • [SMACK-845] - Ensure that IQ response 'to' address and ID are set correctly +
  • +
  • [SMACK-846] - XMPPTCPConnection does not wait for stream features after authentication if compression is disabled +
  • +
  • [SMACK-848] - Make MultiUserChat.leave() wait for response +
  • +
  • [SMACK-850] - DeliveryReceiptManager should not send receipts with messages of type 'groupchat' +
  • +
  • [SMACK-855] - XMPPTCPConnection sometimes has two writer threads running +
  • +
+ +

Improvement +

+
    +
  • [SMACK-847] - Make TCP socket connection attempt interruptable +
  • +
  • [SMACK-849] - Smack Local SOCKS5 Proxy thread should be marked as daemon thread +
  • +
+ +

4.3.1 -- 2018-10-14

+ +

Bug +

+
    +
  • [SMACK-833] - XMLUtil.prettyFormatXml() throws on some Android devices +
  • +
+ +

Improvement +

+
    +
  • [SMACK-829] - Disconnect BOSH client on shutdown +
  • +
  • [SMACK-838] - FormField.getFirstValue() throws IndexOutOfBoundsException if there are no values +
  • +
+

4.3.0 -- 2018-08-02

Bug diff --git a/smack-bosh/build.gradle b/smack-bosh/build.gradle index a3416325e..f686c8209 100644 --- a/smack-bosh/build.gradle +++ b/smack-bosh/build.gradle @@ -4,5 +4,7 @@ This API is considered beta quality.""" dependencies { compile project(':smack-core') - compile 'org.igniterealtime.jbosh:jbosh:[0.9,0.10)' + // See https://issues.igniterealtime.org/browse/SMACK-858 and + // comment in version.gradle why the specify the version this way. + compile 'org.igniterealtime.jbosh:jbosh:[0.9.1,0.9.999]' } diff --git a/smack-bosh/src/main/java/org/jivesoftware/smack/bosh/BOSHConfiguration.java b/smack-bosh/src/main/java/org/jivesoftware/smack/bosh/BOSHConfiguration.java index a83819ad2..8cf01fe85 100644 --- a/smack-bosh/src/main/java/org/jivesoftware/smack/bosh/BOSHConfiguration.java +++ b/smack-bosh/src/main/java/org/jivesoftware/smack/bosh/BOSHConfiguration.java @@ -19,6 +19,8 @@ package org.jivesoftware.smack.bosh; import java.net.URI; import java.net.URISyntaxException; +import java.util.HashMap; +import java.util.Map; import org.jivesoftware.smack.ConnectionConfiguration; import org.jivesoftware.smack.proxy.ProxyInfo; @@ -34,6 +36,7 @@ public final class BOSHConfiguration extends ConnectionConfiguration { private final boolean https; private final String file; + private Map httpHeaders; private BOSHConfiguration(Builder builder) { super(builder); @@ -49,6 +52,7 @@ public final class BOSHConfiguration extends ConnectionConfiguration { } else { file = builder.file; } + httpHeaders = builder.httpHeaders; } public boolean isProxyEnabled() { @@ -76,6 +80,10 @@ public final class BOSHConfiguration extends ConnectionConfiguration { return new URI((https ? "https://" : "http://") + this.host + ":" + this.port + file); } + public Map getHttpHeaders() { + return httpHeaders; + } + public static Builder builder() { return new Builder(); } @@ -83,6 +91,7 @@ public final class BOSHConfiguration extends ConnectionConfiguration { public static final class Builder extends ConnectionConfiguration.Builder { private boolean https; private String file; + private Map httpHeaders = new HashMap<>(); private Builder() { } @@ -101,6 +110,11 @@ public final class BOSHConfiguration extends ConnectionConfiguration { return this; } + public Builder addHttpHeader(String name, String value) { + httpHeaders.put(name, value); + return this; + } + @Override public BOSHConfiguration build() { return new BOSHConfiguration(this); 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 8fba8051a..3c8995b14 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 @@ -22,6 +22,7 @@ import java.io.PipedReader; import java.io.PipedWriter; import java.io.StringReader; import java.io.Writer; +import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; @@ -156,6 +157,9 @@ public class XMPPBOSHConnection extends AbstractXMPPConnection { if (config.isProxyEnabled()) { cfgBuilder.setProxy(config.getProxyAddress(), config.getProxyPort()); } + for (Map.Entry h : config.getHttpHeaders().entrySet()) { + cfgBuilder.addHttpHeader(h.getKey(), h.getValue()); + } client = BOSHClient.create(cfgBuilder.build()); client.addBOSHClientConnListener(new BOSHConnectionListener()); @@ -263,6 +267,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 6ab848c50..fe82c07f6 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/AbstractXMPPConnection.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/AbstractXMPPConnection.java @@ -26,6 +26,7 @@ import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Queue; import java.util.Set; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CopyOnWriteArraySet; @@ -235,7 +236,7 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { * 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( + protected final SynchronizationPoint lastFeaturesReceived = new SynchronizationPoint<>( AbstractXMPPConnection.this, "last stream features received from server"); /** @@ -297,7 +298,18 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { } }); - private static final AsyncButOrdered ASYNC_BUT_ORDERED = new AsyncButOrdered<>(); + protected static final AsyncButOrdered ASYNC_BUT_ORDERED = new AsyncButOrdered<>(); + + /** + * An executor which uses {@link #asyncGoLimited(Runnable)} to limit the number of asynchronously processed runnables + * per connection. + */ + private final Executor limitedExcutor = new Executor() { + @Override + public void execute(Runnable runnable) { + asyncGoLimited(runnable); + } + }; /** * The used host to establish the connection to @@ -314,6 +326,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). @@ -381,6 +396,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. @@ -399,21 +420,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. @@ -550,7 +573,7 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { // - 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(); + lastFeaturesReceived.checkIfSuccessOrWaitOrThrow(); if (!hasFeature(Bind.ELEMENT, Bind.NAMESPACE)) { @@ -582,6 +605,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; @@ -763,6 +789,11 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { */ protected abstract void shutdown(); + /** + * Performs an unclean disconnect and shutdown of the connection. Does not send a closing stream stanza. + */ + public abstract void instantShutdown(); + @Override public void addConnectionListener(ConnectionListener connectionListener) { if (connectionListener == null) { @@ -1097,6 +1128,7 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { if (packet instanceof IQ) { final IQ iq = (IQ) packet; if (iq.isRequestIQ()) { + final IQ iqRequest = iq; final String key = XmppStringUtils.generateKey(iq.getChildElementName(), iq.getChildElementNamespace()); IQRequestHandler iqRequestHandler; final IQ.Type type = iq.getType(); @@ -1146,7 +1178,7 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { executorService = ASYNC_BUT_ORDERED.asExecutorFor(this); break; case async: - executorService = CACHED_EXECUTOR_SERVICE; + executorService = limitedExcutor; break; } final IQRequestHandler finalIqRequestHandler = iqRequestHandler; @@ -1162,6 +1194,11 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { // e.g. to avoid presence leaks. return; } + + assert (response.getType() == IQ.Type.result || response.getType() == IQ.Type.error); + + response.setTo(iqRequest.getFrom()); + response.setStanzaId(iqRequest.getStanzaId()); try { sendStanza(response); } @@ -1191,7 +1228,7 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { } for (final StanzaListener listener : listenersToNotify) { - asyncGo(new Runnable() { + asyncGoLimited(new Runnable() { @Override public void run() { try { @@ -1710,6 +1747,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. @@ -1736,6 +1785,75 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { return getClass().getSimpleName() + '[' + localEndpointString + "] (" + getConnectionCounter() + ')'; } + /** + * A queue of deferred runnables that where not executed immediately because {@link #currentAsyncRunnables} reached + * {@link #maxAsyncRunnables}. Note that we use a {@code LinkedList} in order to avoid space blowups in case the + * list ever becomes very big and shrinks again. + */ + private final Queue deferredAsyncRunnables = new LinkedList<>(); + + private int deferredAsyncRunnablesCount; + + private int deferredAsyncRunnablesCountPrevious; + + private int maxAsyncRunnables = SmackConfiguration.getDefaultConcurrencyLevelLimit(); + + private int currentAsyncRunnables; + + protected void asyncGoLimited(final Runnable runnable) { + Runnable wrappedRunnable = new Runnable() { + @Override + public void run() { + runnable.run(); + + synchronized (deferredAsyncRunnables) { + Runnable defferredRunnable = deferredAsyncRunnables.poll(); + if (defferredRunnable == null) { + currentAsyncRunnables--; + } else { + deferredAsyncRunnablesCount--; + asyncGo(defferredRunnable); + } + } + } + }; + + synchronized (deferredAsyncRunnables) { + if (currentAsyncRunnables < maxAsyncRunnables) { + currentAsyncRunnables++; + asyncGo(wrappedRunnable); + } else { + deferredAsyncRunnablesCount++; + deferredAsyncRunnables.add(wrappedRunnable); + } + + final int HIGH_WATERMARK = 100; + final int INFORM_WATERMARK = 20; + + final int deferredAsyncRunnablesCount = this.deferredAsyncRunnablesCount; + + if (deferredAsyncRunnablesCount >= HIGH_WATERMARK + && deferredAsyncRunnablesCountPrevious < HIGH_WATERMARK) { + LOGGER.log(Level.WARNING, "High watermark of " + HIGH_WATERMARK + " simultaneous executing runnables reached"); + } else if (deferredAsyncRunnablesCount >= INFORM_WATERMARK + && deferredAsyncRunnablesCountPrevious < INFORM_WATERMARK) { + LOGGER.log(Level.INFO, INFORM_WATERMARK + " simultaneous executing runnables reached"); + } + + deferredAsyncRunnablesCountPrevious = deferredAsyncRunnablesCount; + } + } + + public void setMaxAsyncOperations(int maxAsyncOperations) { + if (maxAsyncOperations < 1) { + throw new IllegalArgumentException("Max async operations must be greater than 0"); + } + + synchronized (deferredAsyncRunnables) { + maxAsyncRunnables = maxAsyncOperations; + } + } + protected static void asyncGo(Runnable runnable) { CACHED_EXECUTOR_SERVICE.execute(runnable); } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/AsyncButOrdered.java b/smack-core/src/main/java/org/jivesoftware/smack/AsyncButOrdered.java index 00fb94549..6fb6244ab 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/AsyncButOrdered.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/AsyncButOrdered.java @@ -55,6 +55,16 @@ public class AsyncButOrdered { private final Map threadActiveMap = new WeakHashMap<>(); + private final Executor executor; + + public AsyncButOrdered() { + this(null); + } + + public AsyncButOrdered(Executor executor) { + this.executor = executor; + } + /** * Invoke the given {@link Runnable} asynchronous but ordered in respect to the given key. * @@ -86,7 +96,11 @@ public class AsyncButOrdered { if (newHandler) { Handler handler = new Handler(keyQueue, key); threadActiveMap.put(key, true); - AbstractXMPPConnection.asyncGo(handler); + if (executor == null) { + AbstractXMPPConnection.asyncGo(handler); + } else { + executor.execute(handler); + } } } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/ConnectionConfiguration.java b/smack-core/src/main/java/org/jivesoftware/smack/ConnectionConfiguration.java index c16e5624c..7115a2bc3 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/ConnectionConfiguration.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/ConnectionConfiguration.java @@ -18,6 +18,7 @@ package org.jivesoftware.smack; import java.net.InetAddress; +import java.net.UnknownHostException; import java.security.KeyStore; import java.util.Arrays; import java.util.Collection; @@ -45,6 +46,7 @@ import org.jxmpp.jid.impl.JidCreate; import org.jxmpp.jid.parts.Resourcepart; import org.jxmpp.stringprep.XmppStringprepException; import org.minidns.dnsname.DnsName; +import org.minidns.util.InetAddressUtil; /** * Configuration to use while establishing the connection to the server. @@ -171,6 +173,14 @@ public abstract class ConnectionConfiguration { } + DnsName getHost() { + return host; + } + + InetAddress getHostAddress() { + return hostAddress; + } + /** * Returns the server name of the target server. * @@ -642,6 +652,33 @@ public abstract class ConnectionConfiguration { return getThis(); } + /** + * Set the host to connect to by either its fully qualified domain name (FQDN) or its IP. + * + * @param fqdnOrIp a CharSequence either representing the FQDN or the IP of the host. + * @return a reference to this builder. + * @see #setHost(DnsName) + * @see #setHostAddress(InetAddress) + * @since 4.3.2 + */ + public B setHostAddressByNameOrIp(CharSequence fqdnOrIp) { + String fqdnOrIpString = fqdnOrIp.toString(); + if (InetAddressUtil.isIpAddress(fqdnOrIp)) { + InetAddress hostInetAddress; + try { + hostInetAddress = InetAddress.getByName(fqdnOrIpString); + } + catch (UnknownHostException e) { + // Should never happen. + throw new AssertionError(e); + } + setHostAddress(hostInetAddress); + } else { + setHost(fqdnOrIpString); + } + return getThis(); + } + public B setPort(int port) { if (port < 0 || port > 65535) { throw new IllegalArgumentException( diff --git a/smack-core/src/main/java/org/jivesoftware/smack/SmackConfiguration.java b/smack-core/src/main/java/org/jivesoftware/smack/SmackConfiguration.java index 01f2995a4..10695df47 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/SmackConfiguration.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/SmackConfiguration.java @@ -365,4 +365,19 @@ public final class SmackConfiguration { public static void setUnknownIqRequestReplyMode(UnknownIqRequestReplyMode unknownIqRequestReplyMode) { SmackConfiguration.unknownIqRequestReplyMode = Objects.requireNonNull(unknownIqRequestReplyMode, "Must set mode"); } + + private static final int defaultConcurrencyLevelLimit; + + static { + int availableProcessors = Runtime.getRuntime().availableProcessors(); + if (availableProcessors < 8) { + defaultConcurrencyLevelLimit = 8; + } else { + defaultConcurrencyLevelLimit = (int) (availableProcessors * 1.1); + } + } + + public static int getDefaultConcurrencyLevelLimit() { + return defaultConcurrencyLevelLimit; + } } 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 74d391539..3011a1288 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/SmackException.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/SmackException.java @@ -93,13 +93,24 @@ public class SmackException extends Exception { return new NoResponseException(sb.toString()); } + @Deprecated + // TODO: Remove in Smack 4.4. public static NoResponseException newWith(XMPPConnection connection, StanzaCollector collector) { return newWith(connection, collector.getStanzaFilter()); } + public static NoResponseException newWith(long timeout, + StanzaCollector collector) { + return newWith(timeout, collector.getStanzaFilter()); + } + public static NoResponseException newWith(XMPPConnection connection, StanzaFilter filter) { - final StringBuilder sb = getWaitingFor(connection); + return newWith(connection.getReplyTimeout(), filter); + } + + public static NoResponseException newWith(long timeout, StanzaFilter filter) { + final StringBuilder sb = getWaitingFor(timeout); sb.append(" Waited for response using: "); if (filter != null) { sb.append(filter.toString()); @@ -112,7 +123,10 @@ public 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 (~" @@ -334,4 +348,16 @@ public class SmackException extends Exception { super("Resource binding was not offered by server"); } } + + public static class SmackWrappedException extends SmackException { + + /** + * + */ + private static final long serialVersionUID = 1L; + + public SmackWrappedException(Exception exception) { + super(exception); + } + } } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/SmackFuture.java b/smack-core/src/main/java/org/jivesoftware/smack/SmackFuture.java index 88a951677..9a6511d5f 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/SmackFuture.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/SmackFuture.java @@ -16,11 +16,18 @@ */ package org.jivesoftware.smack; +import java.io.IOException; +import java.net.Socket; +import java.net.SocketAddress; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.net.SocketFactory; import org.jivesoftware.smack.packet.Stanza; import org.jivesoftware.smack.util.CallbackRecipient; @@ -29,6 +36,8 @@ import org.jivesoftware.smack.util.SuccessCallback; public abstract class SmackFuture implements Future, CallbackRecipient { + private static final Logger LOGGER = Logger.getLogger(SmackFuture.class.getName()); + private boolean cancelled; protected V result; @@ -94,7 +103,7 @@ public abstract class SmackFuture implements Future, @Override public final synchronized V get() throws InterruptedException, ExecutionException { while (result == null && exception == null && !cancelled) { - wait(); + futureWait(); } return getOrThrowExecutionException(); @@ -102,7 +111,7 @@ public abstract class SmackFuture implements Future, public final synchronized V getOrThrow() throws E, InterruptedException { while (result == null && exception == null && !cancelled) { - wait(); + futureWait(); } if (exception != null) { @@ -124,7 +133,7 @@ public abstract class SmackFuture implements Future, while (result != null && exception != null) { final long waitTimeRemaining = deadline - System.currentTimeMillis(); if (waitTimeRemaining > 0) { - wait(waitTimeRemaining); + futureWait(waitTimeRemaining); } } @@ -162,6 +171,15 @@ public abstract class SmackFuture implements Future, } } + protected final void futureWait() throws InterruptedException { + futureWait(0); + } + + @SuppressWarnings("WaitNotInLoop") + protected void futureWait(long timeout) throws InterruptedException { + wait(timeout); + } + public static class InternalSmackFuture extends SmackFuture { public final synchronized void setResult(V result) { this.result = result; @@ -178,6 +196,64 @@ public abstract class SmackFuture implements Future, } } + public static class SocketFuture extends InternalSmackFuture { + private final Socket socket; + + private final Object wasInterruptedLock = new Object(); + + private boolean wasInterrupted; + + public SocketFuture(SocketFactory socketFactory) throws IOException { + socket = socketFactory.createSocket(); + } + + @Override + protected void futureWait(long timeout) throws InterruptedException { + try { + super.futureWait(timeout); + } catch (InterruptedException interruptedException) { + synchronized (wasInterruptedLock) { + wasInterrupted = true; + if (!socket.isClosed()) { + closeSocket(); + } + } + throw interruptedException; + } + } + + public void connectAsync(final SocketAddress socketAddress, final int timeout) { + AbstractXMPPConnection.asyncGo(new Runnable() { + @Override + public void run() { + try { + socket.connect(socketAddress, timeout); + } + catch (IOException e) { + setException(e); + return; + } + synchronized (wasInterruptedLock) { + if (wasInterrupted) { + closeSocket(); + return; + } + } + setResult(socket); + } + }); + } + + private void closeSocket() { + try { + socket.close(); + } + catch (IOException ioException) { + LOGGER.log(Level.WARNING, "Could not close socket", ioException); + } + } + } + public abstract static class InternalProcessStanzaSmackFuture extends InternalSmackFuture implements StanzaListener, ExceptionCallback { 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 0b933909e..f6aa5225c 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/StanzaCollector.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/StanzaCollector.java @@ -262,7 +262,7 @@ public class StanzaCollector { if (!connection.isConnected()) { throw new NotConnectedException(connection, packetFilter); } - throw NoResponseException.newWith(connection, this); + throw NoResponseException.newWith(timeout, this); } XMPPErrorException.ifHasErrorThenThrow(result); diff --git a/smack-core/src/main/java/org/jivesoftware/smack/SynchronizationPoint.java b/smack-core/src/main/java/org/jivesoftware/smack/SynchronizationPoint.java index 54be30a36..a40c3aeb5 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/SynchronizationPoint.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/SynchronizationPoint.java @@ -1,6 +1,6 @@ /** * - * Copyright © 2014-2015 Florian Schmaus + * Copyright © 2014-2019 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import java.util.concurrent.locks.Lock; import org.jivesoftware.smack.SmackException.NoResponseException; import org.jivesoftware.smack.SmackException.NotConnectedException; +import org.jivesoftware.smack.SmackException.SmackWrappedException; import org.jivesoftware.smack.packet.Nonza; import org.jivesoftware.smack.packet.Stanza; import org.jivesoftware.smack.packet.TopLevelStreamElement; @@ -37,6 +38,7 @@ public class SynchronizationPoint { // same memory synchronization effects as synchronization block enter and leave. private State state; private E failureException; + private SmackWrappedException smackWrappedExcpetion; /** * Construct a new synchronization point for the given connection. @@ -59,6 +61,7 @@ public class SynchronizationPoint { connectionLock.lock(); state = State.Initial; failureException = null; + smackWrappedExcpetion = null; connectionLock.unlock(); } @@ -71,7 +74,7 @@ public class SynchronizationPoint { * @throws InterruptedException if the connection is interrupted. * @return null if synchronization point was successful, or the failure Exception. */ - public E sendAndWaitForResponse(TopLevelStreamElement request) throws NoResponseException, + public Exception sendAndWaitForResponse(TopLevelStreamElement request) throws NoResponseException, NotConnectedException, InterruptedException { assert (state == State.Initial); connectionLock.lock(); @@ -103,15 +106,14 @@ public class SynchronizationPoint { * @throws NoResponseException if no response was received. * @throws NotConnectedException if the connection is not connected. * @throws InterruptedException if the connection is interrupted. + * @throws SmackWrappedException in case of a wrapped exception; */ public void sendAndWaitForResponseOrThrow(Nonza request) throws E, NoResponseException, - NotConnectedException, InterruptedException { + NotConnectedException, InterruptedException, SmackWrappedException { sendAndWaitForResponse(request); switch (state) { case Failure: - if (failureException != null) { - throw failureException; - } + throwException(); break; default: // Success, do nothing @@ -123,11 +125,12 @@ public class SynchronizationPoint { * @throws NoResponseException if there was no response marking the synchronization point as success or failed. * @throws E if there was a failure * @throws InterruptedException if the connection is interrupted. + * @throws SmackWrappedException in case of a wrapped exception; */ - public void checkIfSuccessOrWaitOrThrow() throws NoResponseException, E, InterruptedException { + public void checkIfSuccessOrWaitOrThrow() throws NoResponseException, E, InterruptedException, SmackWrappedException { checkIfSuccessOrWait(); if (state == State.Failure) { - throw failureException; + throwException(); } } @@ -137,7 +140,7 @@ public class SynchronizationPoint { * @throws InterruptedException * @return null if synchronization point was successful, or the failure Exception. */ - public E checkIfSuccessOrWait() throws NoResponseException, InterruptedException { + public Exception checkIfSuccessOrWait() throws NoResponseException, InterruptedException { connectionLock.lock(); try { switch (state) { @@ -145,7 +148,7 @@ public class SynchronizationPoint { case Success: return null; case Failure: - return failureException; + return getException(); default: // Do nothing break; @@ -198,6 +201,24 @@ public class SynchronizationPoint { } } + /** + * Report this synchronization point as failed because of the given exception. The {@code failureException} must be set. + * + * @param exception the exception causing this synchronization point to fail. + */ + public void reportGenericFailure(SmackWrappedException exception) { + assert exception != null; + connectionLock.lock(); + try { + state = State.Failure; + this.smackWrappedExcpetion = exception; + condition.signalAll(); + } + finally { + connectionLock.unlock(); + } + } + /** * Check if this synchronization point was successful. * @@ -213,6 +234,16 @@ public class SynchronizationPoint { } } + public boolean isNotInInitialState() { + connectionLock.lock(); + try { + return state != State.Initial; + } + finally { + connectionLock.unlock(); + } + } + /** * Check if this synchronization point has its request already sent. * @@ -256,6 +287,20 @@ public class SynchronizationPoint { } } + private Exception getException() { + if (failureException != null) { + return failureException; + } + return smackWrappedExcpetion; + } + + private void throwException() throws E, SmackWrappedException { + if (failureException != null) { + throw failureException; + } + throw smackWrappedExcpetion; + } + /** * Check for a response and throw a {@link NoResponseException} if there was none. *

@@ -264,7 +309,7 @@ public class SynchronizationPoint { * @return true if synchronization point was successful, false on failure. * @throws NoResponseException */ - private E checkForResponse() throws NoResponseException { + private Exception checkForResponse() throws NoResponseException { switch (state) { case Initial: case NoResponse: @@ -273,7 +318,7 @@ public class SynchronizationPoint { case Success: return null; case Failure: - return failureException; + return getException(); default: throw new AssertionError("Unknown state " + state); } 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 6ebd523ab..d5a2dd890 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); } @@ -385,11 +391,6 @@ public class StanzaError extends AbstractError implements ExtensionElement { } public static Condition fromString(String string) { - // Backwards compatibility for older implementations still using RFC 3920. RFC 6120 - // changed 'xml-not-well-formed' to 'not-well-formed'. - if ("xml-not-well-formed".equals(string)) { - string = "not-well-formed"; - } string = string.replace('-', '_'); Condition condition = null; try { diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/StreamError.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/StreamError.java index def48dfbe..67ec3ec2a 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/packet/StreamError.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/StreamError.java @@ -186,6 +186,11 @@ public class StreamError extends AbstractError implements Nonza { } public static Condition fromString(String string) { + // Backwards compatibility for older implementations still using RFC 3920. RFC 6120 + // changed 'xml-not-well-formed' to 'not-well-formed'. + if ("xml-not-well-formed".equals(string)) { + string = "not-well-formed"; + } string = string.replace('-', '_'); Condition condition = null; try { diff --git a/smack-core/src/main/java/org/jivesoftware/smack/provider/IntrospectionProvider.java b/smack-core/src/main/java/org/jivesoftware/smack/provider/IntrospectionProvider.java index 981b437a5..35dde99b4 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/provider/IntrospectionProvider.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/provider/IntrospectionProvider.java @@ -123,7 +123,9 @@ public class IntrospectionProvider{ case "java.lang.String": return value; case "boolean": + // CHECKSTYLE:OFF return Boolean.valueOf(value); + // CHECKSTYLE:ON case "int": return Integer.valueOf(value); case "long": diff --git a/smack-core/src/main/java/org/jivesoftware/smack/proxy/HTTPProxySocketConnection.java b/smack-core/src/main/java/org/jivesoftware/smack/proxy/HTTPProxySocketConnection.java index cd1000c15..549406423 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/proxy/HTTPProxySocketConnection.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/proxy/HTTPProxySocketConnection.java @@ -58,7 +58,7 @@ class HTTPProxySocketConnection implements ProxySocketConnection { proxyLine = "\r\nProxy-Authorization: Basic " + Base64.encode(username + ":" + password); } socket.getOutputStream().write((hostport + " HTTP/1.1\r\nHost: " - + hostport + proxyLine + "\r\n\r\n").getBytes("UTF-8")); + + host + ":" + port + proxyLine + "\r\n\r\n").getBytes("UTF-8")); InputStream in = socket.getInputStream(); StringBuilder got = new StringBuilder(100); @@ -115,7 +115,8 @@ class HTTPProxySocketConnection implements ProxySocketConnection { int code = Integer.parseInt(m.group(1)); if (code != HttpURLConnection.HTTP_OK) { - throw new ProxyException(ProxyInfo.ProxyType.HTTP); + throw new ProxyException(ProxyInfo.ProxyType.HTTP, + "Error code in proxy response: " + code); } } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/ParserUtils.java b/smack-core/src/main/java/org/jivesoftware/smack/util/ParserUtils.java index 28440bdbd..90f2b543c 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/ParserUtils.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/ParserUtils.java @@ -129,6 +129,27 @@ public class ParserUtils { return Resourcepart.from(resourcepartString); } + /** + * Prase a string to a boolean value as per "xs:boolean". Valid input strings are "true", "1" for true, and "false", "0" for false. + * + * @param booleanString the input string. + * @return the boolean representation of the input string + * @throws IllegalArgumentException if the input string is not valid. + * @since 4.3.2 + */ + public static boolean parseXmlBoolean(String booleanString) { + switch (booleanString) { + case "true": + case "1": + return true; + case "false": + case "0": + return false; + default: + throw new IllegalArgumentException(booleanString + " is not a valid boolean string"); + } + } + /** * Get the boolean value of an argument. * @@ -141,7 +162,7 @@ public class ParserUtils { if (valueString == null) return null; valueString = valueString.toLowerCase(Locale.US); - return valueString.equals("true") || valueString.equals("0"); + return parseXmlBoolean(valueString); } public static boolean getBooleanAttribute(XmlPullParser parser, String name, diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/XmlUtil.java b/smack-core/src/main/java/org/jivesoftware/smack/util/XmlUtil.java index 7fc081b43..b55297d6e 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/XmlUtil.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/XmlUtil.java @@ -35,7 +35,11 @@ public class XmlUtil { private static final TransformerFactory transformerFactory = TransformerFactory.newInstance(); static { - transformerFactory.setAttribute("indent-number", 2); + try { + transformerFactory.setAttribute("indent-number", 2); + } catch (IllegalArgumentException e) { + LOGGER.log(Level.INFO, "XML TransformerFactory does not support indent-number attribute", e); + } } public static String prettyFormatXml(CharSequence xml) { diff --git a/smack-core/src/test/java/org/jivesoftware/smack/ConnectionConfigurationTest.java b/smack-core/src/test/java/org/jivesoftware/smack/ConnectionConfigurationTest.java new file mode 100644 index 000000000..6637eae56 --- /dev/null +++ b/smack-core/src/test/java/org/jivesoftware/smack/ConnectionConfigurationTest.java @@ -0,0 +1,79 @@ +/** + * + * Copyright 2018 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 static org.junit.Assert.assertEquals; + +import org.junit.Test; +import org.jxmpp.jid.JidTestUtil; + +public class ConnectionConfigurationTest { + + @Test + public void setIp() { + DummyConnectionConfiguration.Builder builder = newUnitTestBuilder(); + + final String ip = "192.168.0.1"; + builder.setHostAddressByNameOrIp(ip); + + DummyConnectionConfiguration connectionConfiguration = builder.build(); + assertEquals('/' + ip, connectionConfiguration.getHostAddress().toString()); + } + + @Test + public void setFqdn() { + DummyConnectionConfiguration.Builder builder = newUnitTestBuilder(); + + final String fqdn = "foo.example.org"; + builder.setHostAddressByNameOrIp(fqdn); + + DummyConnectionConfiguration connectionConfiguration = builder.build(); + assertEquals(fqdn, connectionConfiguration.getHost().toString()); + } + + private static DummyConnectionConfiguration.Builder newUnitTestBuilder() { + DummyConnectionConfiguration.Builder builder = DummyConnectionConfiguration.builder(); + builder.setXmppDomain(JidTestUtil.DOMAIN_BARE_JID_1); + return builder; + } + + private static final class DummyConnectionConfiguration extends ConnectionConfiguration { + + protected DummyConnectionConfiguration(Builder builder) { + super(builder); + } + + public static Builder builder() { + return new Builder(); + } + + private static final class Builder + extends ConnectionConfiguration.Builder { + + @Override + public DummyConnectionConfiguration build() { + return new DummyConnectionConfiguration(this); + } + + @Override + protected Builder getThis() { + return this; + } + + } + } +} 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 a3ff3246f..ea39e1233 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-core/src/test/java/org/jivesoftware/smack/packet/StreamErrorTest.java b/smack-core/src/test/java/org/jivesoftware/smack/packet/StreamErrorTest.java index f390497f5..515053714 100644 --- a/smack-core/src/test/java/org/jivesoftware/smack/packet/StreamErrorTest.java +++ b/smack-core/src/test/java/org/jivesoftware/smack/packet/StreamErrorTest.java @@ -104,4 +104,22 @@ public class StreamErrorTest { assertNotNull(appSpecificElement); } + @Test + public void testStreamErrorXmlNotWellFormed() { + StreamError error = null; + final String xml = + // Usually the stream:stream element has more attributes (to, version, ...) + // We omit those, since they are not relevant for testing + "" + + "" + + ""; + try { + XmlPullParser parser = PacketParserUtils.getParserFor(xml, "error"); + error = PacketParserUtils.parseStreamError(parser); + } catch (Exception e) { + fail(e.getMessage()); + } + assertNotNull(error); + assertEquals(Condition.not_well_formed, error.getCondition()); + } } diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/hoxt/provider/HttpOverXmppReqProvider.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/hoxt/provider/HttpOverXmppReqProvider.java index 0a69ab4bd..6edf5ca0c 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/hoxt/provider/HttpOverXmppReqProvider.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/hoxt/provider/HttpOverXmppReqProvider.java @@ -16,6 +16,8 @@ */ package org.jivesoftware.smackx.hoxt.provider; +import org.jivesoftware.smack.util.ParserUtils; + import org.jivesoftware.smackx.hoxt.packet.HttpMethod; import org.jivesoftware.smackx.hoxt.packet.HttpOverXmppReq; @@ -47,13 +49,13 @@ public class HttpOverXmppReqProvider extends AbstractHttpOverXmppProvider * {@code * MamQueryArgs mamQueryArgs = MamQueryArgs.builder() - * .withJid(jid) - * .setResultPageSize(10) + * .limitResultsToJid(jid) + * .setResultPageSizeTo(10) * .queryLastPage() * .build(); * MamQuery mamQuery = mamManager.queryArchive(mamQueryArgs); @@ -178,7 +178,9 @@ public final class MamManager extends Manager { * @param connection the XMPP connection to get the archive for. * @return the instance of MamManager. */ + // CHECKSTYLE:OFF:RegexpSingleline public static MamManager getInstanceFor(XMPPConnection connection) { + // CHECKSTYLE:ON:RegexpSingleline return getInstanceFor(connection, (Jid) null); } @@ -995,7 +997,7 @@ public final class MamManager extends Manager { public List pagePrevious(int count) throws NoResponseException, XMPPErrorException, NotConnectedException, NotLoggedInException, InterruptedException { RSMSet previousResultRsmSet = getPreviousRsmSet(); - RSMSet requestRsmSet = new RSMSet(count, previousResultRsmSet.getLast(), RSMSet.PageDirection.before); + RSMSet requestRsmSet = new RSMSet(count, previousResultRsmSet.getFirst(), RSMSet.PageDirection.before); return page(requestRsmSet); } diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/reference/ReferenceManager.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/reference/ReferenceManager.java index d3bdfce12..b588aa5e3 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/reference/ReferenceManager.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/reference/ReferenceManager.java @@ -51,7 +51,7 @@ public final class ReferenceManager extends Manager { * @param connection xmpp connection * @return reference manager instance */ - public static ReferenceManager getInstanceFor(XMPPConnection connection) { + public static synchronized ReferenceManager getInstanceFor(XMPPConnection connection) { ReferenceManager manager = INSTANCES.get(connection); if (manager == null) { manager = new ReferenceManager(connection); 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..a9fd4fcfa 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); } }; @@ -80,7 +78,7 @@ public final class StableUniqueStanzaIdManager extends Manager { * @param connection xmpp-connection * @return manager instance for the connection */ - public static StableUniqueStanzaIdManager getInstanceFor(XMPPConnection connection) { + public static synchronized StableUniqueStanzaIdManager getInstanceFor(XMPPConnection connection) { StableUniqueStanzaIdManager manager = INSTANCES.get(connection); if (manager == null) { manager = new StableUniqueStanzaIdManager(connection); @@ -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 19ef6edd7..a84bf6266 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 @@ -23,7 +23,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) throws Exception { 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 2a3fcad28..a37aaf10f 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 @@ -23,7 +23,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) throws Exception { diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/spoiler/SpoilerManager.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/spoiler/SpoilerManager.java index f3bb80c85..fa5c8680b 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/spoiler/SpoilerManager.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/spoiler/SpoilerManager.java @@ -61,7 +61,7 @@ public final class SpoilerManager extends Manager { * @param connection xmpp connection * @return SpoilerManager */ - public static SpoilerManager getInstanceFor(XMPPConnection connection) { + public static synchronized SpoilerManager getInstanceFor(XMPPConnection connection) { SpoilerManager manager = INSTANCES.get(connection); if (manager == null) { manager = new SpoilerManager(connection); 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 f6486ee43..9b12fc0c3 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(null).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(null).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/bookmarks/Bookmarks.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/bookmarks/Bookmarks.java index 322d0c8bb..f89e40f49 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/bookmarks/Bookmarks.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/bookmarks/Bookmarks.java @@ -269,12 +269,12 @@ public class Bookmarks implements PrivateData { private static BookmarkedConference getConferenceStorage(XmlPullParser parser) throws XmlPullParserException, IOException { String name = parser.getAttributeValue("", "name"); - String autojoin = parser.getAttributeValue("", "autojoin"); + boolean autojoin = ParserUtils.getBooleanAttribute(parser, "autojoin", false); EntityBareJid jid = ParserUtils.getBareJidAttribute(parser); BookmarkedConference conf = new BookmarkedConference(jid); conf.setName(name); - conf.setAutoJoin(Boolean.valueOf(autojoin)); + conf.setAutoJoin(autojoin); // Check for nickname boolean done = false; diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/socks5/Socks5Proxy.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/socks5/Socks5Proxy.java index b39527ee3..da4dde16a 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/socks5/Socks5Proxy.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/socks5/Socks5Proxy.java @@ -211,6 +211,7 @@ public final class Socks5Proxy { if (this.serverSocket != null) { this.serverThread = new Thread(this.serverProcess); this.serverThread.setName("Smack Local SOCKS5 Proxy"); + this.serverThread.setDaemon(true); this.serverThread.start(); } } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/disco/ServiceDiscoveryManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/disco/ServiceDiscoveryManager.java index eabc7773f..06099413d 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/disco/ServiceDiscoveryManager.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/disco/ServiceDiscoveryManager.java @@ -1,6 +1,6 @@ /** * - * Copyright 2003-2007 Jive Software, 2018 Florian Schmaus. + * Copyright 2003-2007 Jive Software, 2018-2019 Florian Schmaus. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -227,7 +227,7 @@ public final class ServiceDiscoveryManager extends Manager { /** * Returns the type of client that will be returned when asked for the client identity in a * disco request. The valid types are defined by the category client. Follow this link to learn - * the possible types: Jabber::Registrar. + * the possible types: XMPP Registry for Service Discovery Identities * * @return the type of client that will be returned when asked for the client identity in a * disco request. @@ -271,8 +271,8 @@ public final class ServiceDiscoveryManager extends Manager { */ public Set getIdentities() { Set res = new HashSet<>(identities); - // Add the default identity that must exist - res.add(defaultIdentity); + // Add the main identity that must exist + res.add(identity); return Collections.unmodifiableSet(res); } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/disco/packet/DiscoverInfo.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/disco/packet/DiscoverInfo.java index 2abda87fb..40cc92909 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/disco/packet/DiscoverInfo.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/disco/packet/DiscoverInfo.java @@ -263,7 +263,7 @@ public class DiscoverInfo extends IQ implements TypedCloneable { * Represents the identity of a given XMPP entity. An entity may have many identities but all * the identities SHOULD have the same name.

* - * Refer to Jabber::Registrar + * Refer to XMPP Registry for Service Discovery Identities * in order to get the official registry of values for the category and type * attributes. * @@ -327,7 +327,7 @@ public class DiscoverInfo extends IQ implements TypedCloneable { /** * Returns the entity's category. To get the official registry of values for the - * 'category' attribute refer to Jabber::Registrar + * 'category' attribute refer to XMPP Registry for Service Discovery Identities. * * @return the entity's category. */ @@ -346,7 +346,7 @@ public class DiscoverInfo extends IQ implements TypedCloneable { /** * Returns the entity's type. To get the official registry of values for the - * 'type' attribute refer to Jabber::Registrar + * 'type' attribute refer to XMPP Registry for Service Discovery Identities. * * @return the entity's type. */ diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/jingle/JingleTransportMethodManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/jingle/JingleTransportMethodManager.java index 848dc1671..9a9941a07 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/jingle/JingleTransportMethodManager.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/jingle/JingleTransportMethodManager.java @@ -48,7 +48,7 @@ public final class JingleTransportMethodManager extends Manager { super(connection); } - public static JingleTransportMethodManager getInstanceFor(XMPPConnection connection) { + public static synchronized JingleTransportMethodManager getInstanceFor(XMPPConnection connection) { JingleTransportMethodManager manager = INSTANCES.get(connection); if (manager == null) { manager = new JingleTransportMethodManager(connection); diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/jingle/transports/jingle_ibb/JingleIBBTransportManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/jingle/transports/jingle_ibb/JingleIBBTransportManager.java index 500a7b0a8..63dada1ae 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/jingle/transports/jingle_ibb/JingleIBBTransportManager.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/jingle/transports/jingle_ibb/JingleIBBTransportManager.java @@ -38,7 +38,7 @@ public final class JingleIBBTransportManager extends JingleTransportManager diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatManager.java index 8c5a98ccd..33e36e76b 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatManager.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatManager.java @@ -19,6 +19,7 @@ package org.jivesoftware.smackx.muc; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -392,19 +393,46 @@ public final class MultiUserChatManager extends Manager { * @throws NotConnectedException * @throws InterruptedException * @throws NotAMucServiceException + * @deprecated use {@link #getRoomsHostedBy(DomainBareJid)} instead. */ + @Deprecated + // TODO: Remove in Smack 4.4. public List getHostedRooms(DomainBareJid serviceName) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, NotAMucServiceException { + Map hostedRooms = getRoomsHostedBy(serviceName); + return new ArrayList<>(hostedRooms.values()); + } + + /** + * Returns a Map of HostedRooms where each HostedRoom has the XMPP address of the room and the room's name. + * Once discovered the rooms hosted by a chat service it is possible to discover more detailed room information or + * join the room. + * + * @param serviceName the service that is hosting the rooms to discover. + * @return a map from the room's address to its HostedRoom information. + * @throws XMPPErrorException + * @throws NoResponseException + * @throws NotConnectedException + * @throws InterruptedException + * @throws NotAMucServiceException + * @since 4.3.1 + */ + public Map getRoomsHostedBy(DomainBareJid serviceName) throws NoResponseException, XMPPErrorException, + NotConnectedException, InterruptedException, NotAMucServiceException { if (!providesMucService(serviceName)) { throw new NotAMucServiceException(serviceName); } ServiceDiscoveryManager discoManager = ServiceDiscoveryManager.getInstanceFor(connection()); DiscoverItems discoverItems = discoManager.discoverItems(serviceName); List items = discoverItems.getItems(); - List answer = new ArrayList(items.size()); + + Map answer = new HashMap<>(items.size()); for (DiscoverItems.Item item : items) { - answer.add(new HostedRoom(item)); + HostedRoom hostedRoom = new HostedRoom(item); + HostedRoom previousRoom = answer.put(hostedRoom.getJid(), hostedRoom); + assert previousRoom == null; } + return answer; } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/RoomInfo.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/RoomInfo.java index b31f5bb4f..421f957f4 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/RoomInfo.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/RoomInfo.java @@ -207,7 +207,8 @@ public class RoomInfo { FormField subjectmodField = form.getField("muc#roominfo_subjectmod"); if (subjectmodField != null && !subjectmodField.getValues().isEmpty()) { - subjectmod = Boolean.valueOf(subjectmodField.getFirstValue()); + String firstValue = subjectmodField.getFirstValue(); + subjectmod = ("true".equals(firstValue) || "1".equals(firstValue)); } FormField urlField = form.getField("muc#roominfo_logs"); diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/receipts/DeliveryReceiptManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/receipts/DeliveryReceiptManager.java index dbfd61436..c310ceb25 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/receipts/DeliveryReceiptManager.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/receipts/DeliveryReceiptManager.java @@ -1,6 +1,6 @@ /** * - * Copyright 2013-2014 Georg Lukas, 2015 Florian Schmaus + * Copyright 2013-2014 Georg Lukas, 2015-2019 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -74,8 +74,16 @@ import org.jxmpp.jid.Jid; */ public final class DeliveryReceiptManager extends Manager { - private static final StanzaFilter MESSAGES_WITH_DELIVERY_RECEIPT_REQUEST = new AndFilter(StanzaTypeFilter.MESSAGE, - new StanzaExtensionFilter(new DeliveryReceiptRequest())); + /** + * Filters all non-error messages with receipt requests. + * See XEP-0184 § 5. "A sender could request receipts + * on any non-error content message (chat, groupchat, headline, or normal)…" + */ + private static final StanzaFilter NON_ERROR_GROUPCHAT_MESSAGES_WITH_DELIVERY_RECEIPT_REQUEST = new AndFilter( + StanzaTypeFilter.MESSAGE, + new StanzaExtensionFilter(new DeliveryReceiptRequest()), + new NotFilter(MessageTypeFilter.ERROR)); + private static final StanzaFilter MESSAGES_WITH_DELIVERY_RECEIPT = new AndFilter(StanzaTypeFilter.MESSAGE, new StanzaExtensionFilter(DeliveryReceipt.ELEMENT, DeliveryReceipt.NAMESPACE)); @@ -175,7 +183,7 @@ public final class DeliveryReceiptManager extends Manager { } connection.sendStanza(ack); } - }, MESSAGES_WITH_DELIVERY_RECEIPT_REQUEST); + }, NON_ERROR_GROUPCHAT_MESSAGES_WITH_DELIVERY_RECEIPT_REQUEST); } /** diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/FormField.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/FormField.java index b45c139bc..5ecf7ab59 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/FormField.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/FormField.java @@ -261,12 +261,14 @@ public class FormField implements NamedElement { */ public String getFirstValue() { CharSequence firstValue; + synchronized (values) { + if (values.isEmpty()) { + return null; + } firstValue = values.get(0); } - if (firstValue == null) { - return null; - } + return firstValue.toString(); } diff --git a/smack-im/src/main/java/org/jivesoftware/smack/roster/Roster.java b/smack-im/src/main/java/org/jivesoftware/smack/roster/Roster.java index c4e7a0cf2..73486cd12 100644 --- a/smack-im/src/main/java/org/jivesoftware/smack/roster/Roster.java +++ b/smack-im/src/main/java/org/jivesoftware/smack/roster/Roster.java @@ -951,8 +951,7 @@ public final class Roster extends Manager { // This is used in case no available presence is found Presence unavailable = null; - for (Resourcepart resource : userPresences.keySet()) { - Presence p = userPresences.get(resource); + for (Presence p : userPresences.values()) { if (!p.isAvailable()) { unavailable = p; continue; @@ -1472,7 +1471,29 @@ public final class Roster extends Manager { final Presence presence = (Presence) packet; final Jid from = presence.getFrom(); - final BareJid key = from != null ? from.asBareJid() : null; + final BareJid key; + if (from != null) { + key = from.asBareJid(); + } else { + XMPPConnection connection = connection(); + if (connection == null) { + LOGGER.finest("Connection was null while trying to handle exotic presence stanza: " + presence); + return; + } + // Assume the presence come "from the users account on the server" since no from was set (RFC 6120 § + // 8.1.2.1 4.). Note that getUser() may return null, but should never return null in this case as where + // connected. + EntityFullJid myJid = connection.getUser(); + if (myJid == null) { + LOGGER.info( + "Connection had no local address in Roster's presence listener." + + " Possibly we received a presence without from before being authenticated." + + " Presence: " + presence); + return; + } + LOGGER.info("Exotic presence stanza without from received: " + presence); + key = myJid.asBareJid(); + } asyncButOrdered.performAsyncButOrdered(key, new Runnable() { @Override diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatIntegrationTest.java index d4898bf16..d57f4467e 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatIntegrationTest.java @@ -16,6 +16,9 @@ */ package org.jivesoftware.smackx.muc; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + import java.util.List; import org.jivesoftware.smack.MessageListener; @@ -23,8 +26,12 @@ import org.jivesoftware.smack.SmackException.NoResponseException; import org.jivesoftware.smack.SmackException.NotConnectedException; import org.jivesoftware.smack.XMPPException.XMPPErrorException; import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.packet.Presence; import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.smackx.muc.MultiUserChat.MucCreateConfigFormHandle; +import org.jivesoftware.smackx.muc.MultiUserChatException.MucNotJoinedException; +import org.jivesoftware.smackx.muc.MultiUserChatException.NotAMucServiceException; +import org.jivesoftware.smackx.muc.packet.MUCUser; import org.igniterealtime.smack.inttest.AbstractSmackIntegrationTest; import org.igniterealtime.smack.inttest.SmackIntegrationTest; @@ -36,6 +43,7 @@ import org.jxmpp.jid.EntityBareJid; import org.jxmpp.jid.impl.JidCreate; import org.jxmpp.jid.parts.Localpart; import org.jxmpp.jid.parts.Resourcepart; +import org.jxmpp.stringprep.XmppStringprepException; public class MultiUserChatIntegrationTest extends AbstractSmackIntegrationTest { @@ -61,6 +69,24 @@ public class MultiUserChatIntegrationTest extends AbstractSmackIntegrationTest { } } + @SmackIntegrationTest + public void mucJoinLeaveTest() throws XmppStringprepException, NotAMucServiceException, NoResponseException, + XMPPErrorException, NotConnectedException, InterruptedException, MucNotJoinedException { + EntityBareJid mucAddress = JidCreate.entityBareFrom(Localpart.from("smack-inttest-join-leave-" + randomString), + mucService.getDomain()); + + MultiUserChat muc = mucManagerOne.getMultiUserChat(mucAddress); + + muc.join(Resourcepart.from("nick-one")); + + Presence reflectedLeavePresence = muc.leaveSync(); + + MUCUser mucUser = MUCUser.from(reflectedLeavePresence); + assertNotNull(mucUser); + + assertTrue(mucUser.getStatus().contains(MUCUser.Status.PRESENCE_TO_SELF_110)); + } + @SmackIntegrationTest public void mucTest() throws Exception { EntityBareJid mucAddress = JidCreate.entityBareFrom(Localpart.from("smack-inttest-" + randomString), mucService.getDomain()); diff --git a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/JingleSession.java b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/JingleSession.java index c7f7b2683..91e2ff035 100644 --- a/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/JingleSession.java +++ b/smack-jingle-old/src/main/java/org/jivesoftware/smackx/jingleold/JingleSession.java @@ -620,7 +620,7 @@ public class JingleSession extends JingleNegotiator implements MediaReceivedList * A XMPP connection * @return a Jingle session */ - public static JingleSession getInstanceFor(XMPPConnection con) { + public static synchronized JingleSession getInstanceFor(XMPPConnection con) { if (con == null) { throw new IllegalArgumentException("XMPPConnection cannot be null"); } diff --git a/smack-legacy/src/main/java/org/jivesoftware/smackx/workgroup/settings/WorkgroupProperties.java b/smack-legacy/src/main/java/org/jivesoftware/smackx/workgroup/settings/WorkgroupProperties.java index c3b07c674..67daae007 100644 --- a/smack-legacy/src/main/java/org/jivesoftware/smackx/workgroup/settings/WorkgroupProperties.java +++ b/smack-legacy/src/main/java/org/jivesoftware/smackx/workgroup/settings/WorkgroupProperties.java @@ -104,7 +104,9 @@ public class WorkgroupProperties extends IQ { while (!done) { int eventType = parser.next(); if ((eventType == XmlPullParser.START_TAG) && ("authRequired".equals(parser.getName()))) { + // CHECKSTYLE:OFF props.setAuthRequired(Boolean.valueOf(parser.nextText()).booleanValue()); + // CHECKSTYLE:ON } else if ((eventType == XmlPullParser.START_TAG) && ("email".equals(parser.getName()))) { props.setEmail(parser.nextText()); 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 12cc680fe..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 @@ -1,6 +1,6 @@ /** * - * Copyright 2013-2018 Florian Schmaus + * Copyright 2013-2019 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,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 * @@ -55,8 +56,8 @@ public class JavaxResolver extends DNSResolver implements SmackInitializer { Hashtable env = new Hashtable<>(); env.put("java.naming.factory.initial", "com.sun.jndi.dns.DnsContextFactory"); dirContext = new InitialDirContext(env); - } catch (Exception e) { - // Ignore. + } catch (NamingException e) { + LOGGER.log(Level.SEVERE, "Could not construct InitialDirContext", e); } // Try to set this DNS resolver as primary one diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/sm/StreamManagementException.java b/smack-tcp/src/main/java/org/jivesoftware/smack/sm/StreamManagementException.java index f5e30784d..553d9d702 100644 --- a/smack-tcp/src/main/java/org/jivesoftware/smack/sm/StreamManagementException.java +++ b/smack-tcp/src/main/java/org/jivesoftware/smack/sm/StreamManagementException.java @@ -16,10 +16,13 @@ */ package org.jivesoftware.smack.sm; +import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.concurrent.BlockingQueue; import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.packet.Element; import org.jivesoftware.smack.packet.Stanza; public abstract class StreamManagementException extends SmackException { @@ -110,5 +113,56 @@ public abstract class StreamManagementException extends SmackException { return ackedStanzas; } } + + public static final class UnacknowledgedQueueFullException extends StreamManagementException { + + /** + * + */ + private static final long serialVersionUID = 1L; + + private final int overflowElementNum; + private final int droppedElements; + private final List elements; + private final List unacknowledgesStanzas; + + private UnacknowledgedQueueFullException(String message, int overflowElementNum, int droppedElements, List elements, + List unacknowledgesStanzas) { + super(message); + this.overflowElementNum = overflowElementNum; + this.droppedElements = droppedElements; + this.elements = elements; + this.unacknowledgesStanzas = unacknowledgesStanzas; + } + + public int getOverflowElementNum() { + return overflowElementNum; + } + + public int getDroppedElements() { + return droppedElements; + } + + public List getElements() { + return elements; + } + + public List getUnacknowledgesStanzas() { + return unacknowledgesStanzas; + } + + public static UnacknowledgedQueueFullException newWith(int overflowElementNum, List elements, + BlockingQueue unacknowledgedStanzas) { + final int unacknowledgesStanzasQueueSize = unacknowledgedStanzas.size(); + List localUnacknowledgesStanzas = new ArrayList<>(unacknowledgesStanzasQueueSize); + localUnacknowledgesStanzas.addAll(unacknowledgedStanzas); + int droppedElements = elements.size() - overflowElementNum - 1; + + String message = "The queue size " + unacknowledgesStanzasQueueSize + " is not able to fit another " + + droppedElements + " potential stanzas type top-level stream-elements."; + return new UnacknowledgedQueueFullException(message, overflowElementNum, droppedElements, elements, + localUnacknowledgesStanzas); + } + } } 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 b48e3cf4b..a08221ddb 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 @@ -51,6 +51,7 @@ import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; @@ -83,6 +84,8 @@ import org.jivesoftware.smack.SmackException.NoResponseException; import org.jivesoftware.smack.SmackException.NotConnectedException; import org.jivesoftware.smack.SmackException.NotLoggedInException; import org.jivesoftware.smack.SmackException.SecurityRequiredByServerException; +import org.jivesoftware.smack.SmackException.SmackWrappedException; +import org.jivesoftware.smack.SmackFuture; import org.jivesoftware.smack.StanzaListener; import org.jivesoftware.smack.SynchronizationPoint; import org.jivesoftware.smack.XMPPConnection; @@ -165,15 +168,17 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { private SSLSocket secureSocket; - /** - * Protected access level because of unit test purposes - */ - protected PacketWriter packetWriter; + private final Semaphore readerWriterSemaphore = new Semaphore(2); /** * Protected access level because of unit test purposes */ - protected PacketReader packetReader; + protected final PacketWriter packetWriter = new PacketWriter(); + + /** + * Protected access level because of unit test purposes + */ + protected final PacketReader packetReader = new PacketReader(); private final SynchronizationPoint initialOpenStreamSend = new SynchronizationPoint<>( this, "initial open stream element send to server"); @@ -278,6 +283,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. @@ -387,6 +401,11 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { SSLSession sslSession = secureSocket != null ? secureSocket.getSession() : null; saslAuthentication.authenticate(username, password, config.getAuthzid(), sslSession); + // Wait for stream features after the authentication. + // TODO: The name of this synchronization point "maybeCompressFeaturesReceived" is not perfect. It should be + // renamed to "streamFeaturesAfterAuthenticationReceived". + maybeCompressFeaturesReceived.checkIfSuccessOrWait(); + // If compression is enabled then request the server to use stream compression. XEP-170 // recommends to perform stream compression before resource binding. maybeEnableCompression(); @@ -437,9 +456,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); @@ -468,24 +502,16 @@ 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); } private void shutdown(boolean instant) { - if (disconnectedButResumeable) { - return; - } - // First shutdown the writer, this will result in a closing stream element getting send to // the server - if (packetWriter != null) { - LOGGER.finer("PacketWriter shutdown()"); - packetWriter.shutdown(instant); - } + LOGGER.finer("PacketWriter shutdown()"); + packetWriter.shutdown(instant); LOGGER.finer("PacketWriter has been shut down"); if (!instant) { @@ -500,19 +526,29 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { } } - if (packetReader != null) { - LOGGER.finer("PacketReader shutdown()"); - packetReader.shutdown(); - } + LOGGER.finer("PacketReader shutdown()"); + packetReader.shutdown(); LOGGER.finer("PacketReader has been shut down"); - try { + final Socket socket = this.socket; + if (socket != null && socket.isConnected()) { + try { socket.close(); - } catch (Exception e) { + } catch (Exception e) { LOGGER.log(Level.WARNING, "shutdown", e); + } } setWasAuthenticated(); + + // Wait for reader and writer threads to be terminated. + readerWriterSemaphore.acquireUninterruptibly(2); + readerWriterSemaphore.release(2); + + if (disconnectedButResumeable) { + return; + } + // 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. sendStanza should not throw a NotConnectedException). @@ -523,6 +559,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; @@ -530,6 +568,12 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { reader = null; writer = null; + initState(); + } + + @Override + protected void initState() { + super.initState(); maybeCompressFeaturesReceived.init(); compressSyncPoint.init(); smResumedSyncPoint.init(); @@ -555,7 +599,7 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { } } - private void connectUsingConfiguration() throws ConnectionException, IOException { + private void connectUsingConfiguration() throws ConnectionException, IOException, InterruptedException { List failedAddresses = populateHostAddresses(); SocketFactory socketFactory = config.getSocketFactory(); ProxyInfo proxyInfo = config.getProxyInfo(); @@ -574,14 +618,16 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { innerloop: while (inetAddresses.hasNext()) { // Create a *new* Socket before every connection attempt, i.e. connect() call, since Sockets are not // re-usable after a failed connection attempt. See also SMACK-724. - socket = socketFactory.createSocket(); + SmackFuture.SocketFuture socketFuture = new SmackFuture.SocketFuture(socketFactory); final InetAddress inetAddress = inetAddresses.next(); - final String inetAddressAndPort = inetAddress + " at port " + port; - LOGGER.finer("Trying to establish TCP connection to " + inetAddressAndPort); + final InetSocketAddress inetSocketAddress = new InetSocketAddress(inetAddress, port); + LOGGER.finer("Trying to establish TCP connection to " + inetSocketAddress); + socketFuture.connectAsync(inetSocketAddress, timeout); + try { - socket.connect(new InetSocketAddress(inetAddress, port), timeout); - } catch (Exception e) { + socket = socketFuture.getOrThrow(); + } catch (IOException e) { hostAddress.setException(inetAddress, e); if (inetAddresses.hasNext()) { continue innerloop; @@ -589,7 +635,7 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { break innerloop; } } - LOGGER.finer("Established TCP connection to " + inetAddressAndPort); + LOGGER.finer("Established TCP connection to " + inetSocketAddress); // We found a host to connect to, return here this.host = host; this.port = port; @@ -605,6 +651,7 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { proxyInfo.getProxySocketConnection().connect(socket, host, port, timeout); } catch (IOException e) { hostAddress.setException(e); + failedAddresses.add(hostAddress); continue; } LOGGER.finer("Established TCP connection to " + hostAndPort); @@ -627,18 +674,23 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { * @throws XMPPException if establishing a connection to the server fails. * @throws SmackException if the server fails to respond back or if there is anther error. * @throws IOException + * @throws InterruptedException */ - private void initConnection() throws IOException { - boolean isFirstInitialization = packetReader == null || packetWriter == null; + private void initConnection() throws IOException, InterruptedException { compressionHandler = null; // Set the reader and writer instance variables initReaderAndWriter(); - if (isFirstInitialization) { - packetWriter = new PacketWriter(); - packetReader = new PacketReader(); + int availableReaderWriterSemaphorePermits = readerWriterSemaphore.availablePermits(); + if (availableReaderWriterSemaphorePermits < 2) { + Object[] logObjects = new Object[] { + this, + availableReaderWriterSemaphorePermits, + }; + LOGGER.log(Level.FINE, "Not every reader/writer threads where terminated on connection re-initializtion of {0}. Available permits {1}", logObjects); } + readerWriterSemaphore.acquire(2); // Start the writer thread. This will open an XMPP stream to the server packetWriter.init(); // Start the reader thread. The startup() method will block until we @@ -859,7 +911,7 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { if (!config.isCompressionEnabled()) { return; } - maybeCompressFeaturesReceived.checkIfSuccessOrWait(); + Compress.Feature compression = getFeature(Compress.Feature.ELEMENT, Compress.NAMESPACE); if (compression == null) { // Server does not support compression @@ -910,18 +962,42 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { * * @param e the exception that causes the connection close event. */ - 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; + private void notifyConnectionError(final Exception e) { + ASYNC_BUT_ORDERED.performAsyncButOrdered(this, new Runnable() { + @Override + public void run() { + // Listeners were already notified of the exception, return right here. + if (packetReader.done || packetWriter.done()) return; - // Closes the connection temporary. A reconnection is possible - // Note that a connection listener of XMPPTCPConnection will drop the SM state in - // case the Exception is a StreamErrorException. - instantShutdown(); + // Report the failure outside the synchronized block, so that a thread waiting within a synchronized + // function like connect() throws the wrapped exception. + SmackWrappedException smackWrappedException = new SmackWrappedException(e); + tlsHandled.reportGenericFailure(smackWrappedException); + saslFeatureReceived.reportGenericFailure(smackWrappedException); + maybeCompressFeaturesReceived.reportGenericFailure(smackWrappedException); + lastFeaturesReceived.reportGenericFailure(smackWrappedException); - // Notify connection listeners of the error. - callConnectionClosedOnErrorListener(e); + synchronized (XMPPTCPConnection.this) { + // Within this synchronized block, either *both* reader and writer threads must be terminated, or + // none. + assert ((packetReader.done && packetWriter.done()) + || (!packetReader.done && !packetWriter.done())); + + // Closes the connection temporary. A reconnection is possible + // Note that a connection listener of XMPPTCPConnection will drop the SM state in + // case the Exception is a StreamErrorException. + instantShutdown(); + } + + Async.go(new Runnable() { + @Override + public void run() { + // Notify connection listeners of the error. + callConnectionClosedOnErrorListener(e); + } + }, XMPPTCPConnection.this + " callConnectionClosedOnErrorListener()"); + } + }); } /** @@ -934,14 +1010,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) { @@ -992,6 +1067,8 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { protected class PacketReader { + private final String threadName = "Smack Reader (" + getConnectionCounter() + ')'; + XmlPullParser parser; private volatile boolean done; @@ -1006,9 +1083,15 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { Async.go(new Runnable() { @Override public void run() { - parsePackets(); + LOGGER.finer(threadName + " start"); + try { + parsePackets(); + } finally { + LOGGER.finer(threadName + " exit"); + XMPPTCPConnection.this.readerWriterSemaphore.release(); + } } - }, "Smack Reader (" + getConnectionCounter() + ")"); + }, threadName); } /** @@ -1228,7 +1311,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; @@ -1257,6 +1344,8 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { protected class PacketWriter { public static final int QUEUE_SIZE = XMPPTCPConnection.QUEUE_SIZE; + private final String threadName = "Smack Writer (" + getConnectionCounter() + ')'; + private final ArrayBlockingQueueWithShutdown queue = new ArrayBlockingQueueWithShutdown<>( QUEUE_SIZE, true); @@ -1302,9 +1391,15 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { Async.go(new Runnable() { @Override public void run() { - writePackets(); + LOGGER.finer(threadName + " start"); + try { + writePackets(); + } finally { + LOGGER.finer(threadName + " exit"); + XMPPTCPConnection.this.readerWriterSemaphore.release(); + } } - }, "Smack Writer (" + getConnectionCounter() + ")"); + }, threadName); } private boolean done() { @@ -1355,11 +1450,12 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { instantShutdown = instant; queue.shutdown(); shutdownTimestamp = System.currentTimeMillis(); - try { - shutdownDone.checkIfSuccessOrWait(); - } - catch (NoResponseException | InterruptedException e) { - LOGGER.log(Level.WARNING, "shutdownDone was not marked as successful by the writer thread", e); + if (shutdownDone.isNotInInitialState()) { + try { + shutdownDone.checkIfSuccessOrWait(); + } catch (NoResponseException | InterruptedException e) { + LOGGER.log(Level.WARNING, "shutdownDone was not marked as successful by the writer thread", e); + } } } @@ -1514,7 +1610,16 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { private void drainWriterQueueToUnacknowledgedStanzas() { List elements = new ArrayList<>(queue.size()); queue.drainTo(elements); - for (Element element : elements) { + for (int i = 0; i < elements.size(); i++) { + Element element = elements.get(i); + // If the unacknowledgedStanza queue is full, then bail out with a warning message. See SMACK-844. + if (unacknowledgedStanzas.remainingCapacity() == 0) { + StreamManagementException.UnacknowledgedQueueFullException exception = StreamManagementException.UnacknowledgedQueueFullException + .newWith(i, elements, unacknowledgedStanzas); + LOGGER.log(Level.WARNING, + "Some stanzas may be lost as not all could be drained to the unacknowledged stanzas queue", exception); + return; + } if (element instanceof Stanza) { unacknowledgedStanzas.add((Stanza) element); } @@ -1719,6 +1824,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/smack-tcp/src/test/java/org/jivesoftware/smack/tcp/PacketWriterTest.java b/smack-tcp/src/test/java/org/jivesoftware/smack/tcp/PacketWriterTest.java index c72996783..c28ec85e4 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 @@ -1,6 +1,6 @@ /** * - * Copyright 2014 Florian Schmaus + * Copyright 2014-2019 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -49,11 +49,9 @@ public class PacketWriterTest { @Test public void shouldBlockAndUnblockTest() throws InterruptedException, BrokenBarrierException, NotConnectedException, XmppStringprepException { XMPPTCPConnection connection = new XMPPTCPConnection("user", "pass", "example.org"); - final PacketWriter pw = connection.new PacketWriter(); - connection.packetWriter = pw; - connection.packetReader = connection.new PacketReader(); + final PacketWriter pw = connection.packetWriter; connection.setWriter(new BlockingStringWriter()); - pw.init(); + connection.packetWriter.init(); for (int i = 0; i < XMPPTCPConnection.PacketWriter.QUEUE_SIZE; i++) { pw.sendStreamElement(new Message()); diff --git a/version.gradle b/version.gradle index 683627553..82e39bdf3 100644 --- a/version.gradle +++ b/version.gradle @@ -1,9 +1,15 @@ allprojects { ext { - shortVersion = '4.3.1' + shortVersion = '4.3.4' isSnapshot = true - jxmppVersion = '0.6.3' - miniDnsVersion = '0.3.2' + // 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.6, 0.6.999]' + miniDnsVersion = '[0.3, 0.3.999]' smackMinAndroidSdk = 9 } }