diff --git a/build.gradle b/build.gradle index 0bdce4c74..96dcab393 100644 --- a/build.gradle +++ b/build.gradle @@ -662,14 +662,6 @@ task omemoSignalIntTest { dependsOn project(':smack-omemo-signal-integration-test').tasks.run } -task sinttestAll { - description 'Run all of Smack\'s integration tests.' - dependsOn {[ - integrationTest, - omemoSignalIntTest, - ]} -} - def getGitCommit() { def dotGit = new File("$projectDir/.git") if (!dotGit.isDirectory()) return 'non-git build' 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 6e90e35b2..136976a09 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/AbstractXMPPConnection.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/AbstractXMPPConnection.java @@ -111,7 +111,6 @@ import org.jivesoftware.smack.util.PacketParserUtils; import org.jivesoftware.smack.util.ParserUtils; import org.jivesoftware.smack.util.Predicate; import org.jivesoftware.smack.util.StringUtils; -import org.jivesoftware.smack.util.Supplier; import org.jivesoftware.smack.xml.XmlPullParser; import org.jivesoftware.smack.xml.XmlPullParserException; @@ -175,12 +174,6 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { SmackConfiguration.getVersion(); } - protected enum SyncPointState { - initial, - request_sent, - successful, - } - /** * A collection of ConnectionListeners which listen for connection closing * and reconnection events. @@ -278,29 +271,30 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { */ protected Writer writer; - protected SmackException currentSmackException; - protected XMPPException currentXmppException; - - protected boolean tlsHandled; + protected final SynchronizationPoint tlsHandled = new SynchronizationPoint<>(this, "establishing TLS"); /** - * Set to true if the last features stanza from the server has been parsed. A XMPP connection + * Set to success if the last features stanza from the server has been parsed. A XMPP connection * handshake can invoke multiple features stanzas, e.g. when TLS is activated a second feature * stanza is send by the server. This is set to true once the last feature stanza has been * parsed. */ - protected boolean lastFeaturesReceived; + protected final SynchronizationPoint lastFeaturesReceived = new SynchronizationPoint<>( + AbstractXMPPConnection.this, "last stream features received from server"); /** - * Set to true if the SASL feature has been received. + * Set to success if the SASL feature has been received. */ - protected boolean saslFeatureReceived; + protected final SynchronizationPoint saslFeatureReceived = new SynchronizationPoint<>( + AbstractXMPPConnection.this, "SASL mechanisms stream feature from server"); + /** * A synchronization point which is successful if this connection has received the closing * stream element from the remote end-point, i.e. the server. */ - protected boolean closingStreamReceived; + protected final SynchronizationPoint closingStreamReceived = new SynchronizationPoint<>( + this, "stream closing element received"); /** * The SASLAuthentication manager that is responsible for authenticating with the server. @@ -375,6 +369,8 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { */ protected boolean wasAuthenticated = false; + protected Exception currentConnectionException; + private final Map setIqRequestHandler = new HashMap<>(); private final Map getIqRequestHandler = new HashMap<>(); @@ -490,10 +486,10 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { public abstract boolean isUsingCompression(); protected void initState() { - currentSmackException = null; - currentXmppException = null; - saslFeatureReceived = lastFeaturesReceived = tlsHandled = false; - // TODO: We do not init closingStreamReceived here, as the integration tests use it to check if we waited for + saslFeatureReceived.init(); + lastFeaturesReceived.init(); + tlsHandled.init(); + // TODO: We do not init() closingStreamReceived here, as the integration tests use it to check if we waited for // it. } @@ -516,7 +512,7 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { // Reset the connection state initState(); - closingStreamReceived = false; + closingStreamReceived.init(); streamId = null; try { @@ -661,82 +657,15 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { return streamId; } - protected final void throwCurrentConnectionException() throws SmackException, XMPPException { - if (currentSmackException != null) { - throw currentSmackException; - } else if (currentXmppException != null) { - throw currentXmppException; - } + protected Resourcepart bindResourceAndEstablishSession(Resourcepart resource) throws XMPPErrorException, + SmackException, InterruptedException { - throw new AssertionError("No current connection exception set, although throwCurrentException() was called"); - } - - protected final boolean hasCurrentConnectionException() { - return currentSmackException != null || currentXmppException != null; - } - - protected final void setCurrentConnectionExceptionAndNotify(Exception exception) { - if (exception instanceof SmackException) { - currentSmackException = (SmackException) exception; - } else if (exception instanceof XMPPException) { - currentXmppException = (XMPPException) exception; - } else { - currentSmackException = new SmackException.SmackWrappedException(exception); - } - - notifyWaitingThreads(); - } - - /** - * We use an extra object for {@link #notifyWaitingThreads()} and {@link #waitForCondition(Supplier)}, because all state - * changing methods of the connection are synchronized using the connection instance as monitor. If we now would - * also use the connection instance for the internal process to wait for a condition, the {@link Object#wait()} - * would leave the monitor when it waites, which would allow for another potential call to a state changing function - * to proceed. - */ - private final Object internalMonitor = new Object(); - - protected final void notifyWaitingThreads() { - synchronized (internalMonitor) { - internalMonitor.notifyAll(); - } - } - - protected final boolean waitForCondition(Supplier condition) throws InterruptedException { - final long deadline = System.currentTimeMillis() + getReplyTimeout(); - synchronized (internalMonitor) { - while (!condition.get().booleanValue() && !hasCurrentConnectionException()) { - final long now = System.currentTimeMillis(); - if (now >= deadline) { - return false; - } - internalMonitor.wait(deadline - now); - } - } - return true; - } - - protected final void waitForCondition(Supplier condition, String waitFor) throws InterruptedException, NoResponseException { - boolean success = waitForCondition(condition); - if (!success) { - throw NoResponseException.newWith(this, waitFor); - } - } - - protected final void waitForConditionOrThrowConnectionException(Supplier condition, String waitFor) throws InterruptedException, SmackException, XMPPException { - waitForCondition(condition, waitFor); - if (hasCurrentConnectionException()) { - throwCurrentConnectionException(); - } - } - - protected Resourcepart bindResourceAndEstablishSession(Resourcepart resource) - throws SmackException, InterruptedException, XMPPException { // Wait until either: // - the servers last features stanza has been parsed // - the timeout occurs LOGGER.finer("Waiting for last features to be received before continuing with resource binding"); - waitForConditionOrThrowConnectionException(() -> lastFeaturesReceived, "last stream features received from server"); + lastFeaturesReceived.checkIfSuccessOrWaitOrThrow(); + if (!hasFeature(Bind.ELEMENT, Bind.NAMESPACE)) { // Server never offered resource binding, which is REQUIRED in XMPP client and @@ -963,8 +892,6 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { callConnectionClosedListener(); } - private final Object notifyConnectionErrorMonitor = new Object(); - /** * Sends out a notification that there was an error with the connection * and closes the connection. @@ -972,31 +899,41 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { * @param exception the exception that causes the connection close event. */ protected final void notifyConnectionError(final Exception exception) { - synchronized (notifyConnectionErrorMonitor) { - if (!isConnected()) { - LOGGER.log(Level.INFO, "Connection was already disconnected when attempting to handle " + exception, - exception); - return; - } + if (!isConnected()) { + LOGGER.log(Level.INFO, "Connection was already disconnected when attempting to handle " + exception, + exception); + return; + } - // Note that we first have to set the current connection exception and notify waiting threads, as one of them - // could hold the instance lock, which we also need later when calling instantShutdown(). - setCurrentConnectionExceptionAndNotify(exception); - - // Closes the connection temporary. A if the connection supports stream management, then a reconnection is - // possible. Note that a connection listener of e.g. XMPPTCPConnection will drop the SM state in - // case the Exception is a StreamErrorException. - instantShutdown(); + ASYNC_BUT_ORDERED.performAsyncButOrdered(this, () -> { + currentConnectionException = exception; for (StanzaCollector collector : collectors) { collector.notifyConnectionError(exception); } + SmackWrappedException smackWrappedException = new SmackWrappedException(exception); + tlsHandled.reportGenericFailure(smackWrappedException); + saslFeatureReceived.reportGenericFailure(smackWrappedException); + lastFeaturesReceived.reportGenericFailure(smackWrappedException); + closingStreamReceived.reportFailure(smackWrappedException); + // TODO From XMPPTCPConnection. Was called in Smack 4.3 where notifyConnectionError() was part of + // XMPPTCPConnection. Create delegation method? + // maybeCompressFeaturesReceived.reportGenericFailure(smackWrappedException); + + synchronized (AbstractXMPPConnection.this) { + notifyAll(); + + // Closes the connection temporary. A if the connection supports stream management, then a reconnection is + // possible. Note that a connection listener of e.g. XMPPTCPConnection will drop the SM state in + // case the Exception is a StreamErrorException. + instantShutdown(); + } Async.go(() -> { // Notify connection listeners of the error. callConnectionClosedOnErrorListener(exception); }, AbstractXMPPConnection.this + " callConnectionClosedOnErrorListener()"); - } + }); } /** @@ -1010,13 +947,19 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { protected abstract void shutdown(); protected final boolean waitForClosingStreamTagFromServer() { + Exception exception; try { - waitForConditionOrThrowConnectionException(() -> closingStreamReceived, "closing stream tag from the server"); - } catch (InterruptedException | SmackException | XMPPException e) { - LOGGER.log(Level.INFO, "Exception while waiting for closing stream element from the server " + this, e); - return false; + // After we send the closing stream element, check if there was already a + // closing stream element sent by the server or wait with a timeout for a + // closing stream element to be received from the server. + exception = closingStreamReceived.checkIfSuccessOrWait(); + } catch (InterruptedException | NoResponseException e) { + exception = e; } - return true; + if (exception != null) { + LOGGER.log(Level.INFO, "Exception while waiting for closing stream element from the server " + this, exception); + } + return exception == null; } @Override @@ -1874,8 +1817,8 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { // Only proceed with SASL auth if TLS is disabled or if the server doesn't announce it if (!hasFeature(StartTls.ELEMENT, StartTls.NAMESPACE) || config.getSecurityMode() == SecurityMode.disabled) { - tlsHandled = saslFeatureReceived = true; - notifyWaitingThreads(); + tlsHandled.reportSuccess(); + saslFeatureReceived.reportSuccess(); } } @@ -1884,10 +1827,9 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { if (hasFeature(Bind.ELEMENT, Bind.NAMESPACE)) { if (!hasFeature(Compress.Feature.ELEMENT, Compress.NAMESPACE) || !config.isCompressionEnabled()) { - // This where the last stream features from the server, either it did not contain - // compression or we disabled it. - lastFeaturesReceived = true; - notifyWaitingThreads(); + // This was was last features from the server is either it did not contain + // compression or if we disabled it + lastFeaturesReceived.reportSuccess(); } } afterFeaturesReceived(); 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 3d2c0b851..f58ed10a4 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/SmackException.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/SmackException.java @@ -16,7 +16,6 @@ */ package org.jivesoftware.smack; -import java.security.cert.CertificateException; import java.util.Collections; import java.util.List; @@ -372,6 +371,15 @@ public abstract class SmackException extends Exception { } } + public static class ConnectionUnexpectedTerminatedException extends SmackException { + + private static final long serialVersionUID = 1L; + + public ConnectionUnexpectedTerminatedException(Throwable wrappedThrowable) { + super(wrappedThrowable); + } + } + public static class FeatureNotSupportedException extends SmackException { /** @@ -479,19 +487,4 @@ public abstract class SmackException extends Exception { super(message, exception); } } - - public static class SmackCertificateException extends SmackException { - - private static final long serialVersionUID = 1L; - - private final CertificateException certificateException; - - public SmackCertificateException(CertificateException certificateException) { - this.certificateException = certificateException; - } - - public CertificateException getCertificateException() { - return certificateException; - } - } } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/SmackReactor.java b/smack-core/src/main/java/org/jivesoftware/smack/SmackReactor.java index 75e1af8e9..eaff9ca3d 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/SmackReactor.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/SmackReactor.java @@ -17,6 +17,7 @@ package org.jivesoftware.smack; import java.io.IOException; +import java.lang.ref.WeakReference; import java.nio.channels.CancelledKeyException; import java.nio.channels.ClosedChannelException; import java.nio.channels.SelectableChannel; @@ -367,8 +368,13 @@ public class SmackReactor { for (SelectionKey selectionKey : selectedKeys) { SelectableChannel channel = selectionKey.channel(); SelectionKeyAttachment selectionKeyAttachment = (SelectionKeyAttachment) selectionKey.attachment(); - ChannelSelectedCallback channelSelectedCallback = selectionKeyAttachment.channelSelectedCallback; - channelSelectedCallback.onChannelSelected(channel, selectionKey); + ChannelSelectedCallback channelSelectedCallback = selectionKeyAttachment.weaeklyReferencedChannelSelectedCallback.get(); + if (channelSelectedCallback != null) { + channelSelectedCallback.onChannelSelected(channel, selectionKey); + } + else { + selectionKey.cancel(); + } } } @@ -416,11 +422,11 @@ public class SmackReactor { } public static final class SelectionKeyAttachment { - private final ChannelSelectedCallback channelSelectedCallback; + private final WeakReference weaeklyReferencedChannelSelectedCallback; private final AtomicBoolean reactorThreadRacing = new AtomicBoolean(); private SelectionKeyAttachment(ChannelSelectedCallback channelSelectedCallback) { - this.channelSelectedCallback = channelSelectedCallback; + this.weaeklyReferencedChannelSelectedCallback = new WeakReference<>(channelSelectedCallback); } private void setRacing() { diff --git a/smack-core/src/main/java/org/jivesoftware/smack/SynchronizationPoint.java b/smack-core/src/main/java/org/jivesoftware/smack/SynchronizationPoint.java new file mode 100644 index 000000000..7652a48e0 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/SynchronizationPoint.java @@ -0,0 +1,352 @@ +/** + * + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; + +import 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; + +public class SynchronizationPoint { + + private final AbstractXMPPConnection connection; + private final Lock connectionLock; + private final Condition condition; + private final String waitFor; + + // Note that there is no need to make 'state' and 'failureException' volatile. Since 'lock' and 'unlock' have the + // same memory synchronization effects as synchronization block enter and leave. + private State state; + private E failureException; + private SmackWrappedException smackWrappedExcpetion; + + private volatile long waitStart; + + /** + * Construct a new synchronization point for the given connection. + * + * @param connection the connection of this synchronization point. + * @param waitFor a description of the event this synchronization point handles. + */ + public SynchronizationPoint(AbstractXMPPConnection connection, String waitFor) { + this.connection = connection; + this.connectionLock = connection.getConnectionLock(); + this.condition = connection.getConnectionLock().newCondition(); + this.waitFor = waitFor; + init(); + } + + /** + * Initialize (or reset) this synchronization point. + */ + @SuppressWarnings("LockNotBeforeTry") + public void init() { + connectionLock.lock(); + state = State.Initial; + failureException = null; + smackWrappedExcpetion = null; + connectionLock.unlock(); + } + + /** + * Send the given top level stream element and wait for a response. + * + * @param request the plain stream element to send. + * @throws NoResponseException if no response was received. + * @throws NotConnectedException if the connection is not connected. + * @throws InterruptedException if the connection is interrupted. + * @return null if synchronization point was successful, or the failure Exception. + */ + public Exception sendAndWaitForResponse(TopLevelStreamElement request) throws NoResponseException, + NotConnectedException, InterruptedException { + assert state == State.Initial; + connectionLock.lock(); + try { + if (request != null) { + if (request instanceof Stanza) { + connection.sendStanza((Stanza) request); + } + else if (request instanceof Nonza) { + connection.sendNonza((Nonza) request); + } else { + throw new IllegalStateException("Unsupported element type"); + } + state = State.RequestSent; + } + waitForConditionOrTimeout(); + } + finally { + connectionLock.unlock(); + } + return checkForResponse(); + } + + /** + * Send the given plain stream element and wait for a response. + * + * @param request the plain stream element to send. + * @throws E if an failure was reported. + * @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, SmackWrappedException { + sendAndWaitForResponse(request); + switch (state) { + case Failure: + throwException(); + break; + default: + // Success, do nothing + } + } + + /** + * Check if this synchronization point is successful or wait the connections reply timeout. + * @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, SmackWrappedException { + checkIfSuccessOrWait(); + if (state == State.Failure) { + throwException(); + } + } + + /** + * Check if this synchronization point is successful or wait the connections reply timeout. + * @throws NoResponseException if there was no response marking the synchronization point as success or failed. + * @throws InterruptedException if the calling thread was interrupted. + * @return null if synchronization point was successful, or the failure Exception. + */ + public Exception checkIfSuccessOrWait() throws NoResponseException, InterruptedException { + connectionLock.lock(); + try { + switch (state) { + // Return immediately on success or failure + case Success: + return null; + case Failure: + return getException(); + default: + // Do nothing + break; + } + waitForConditionOrTimeout(); + } finally { + connectionLock.unlock(); + } + return checkForResponse(); + } + + /** + * Report this synchronization point as successful. + */ + public void reportSuccess() { + connectionLock.lock(); + try { + state = State.Success; + condition.signalAll(); + } + finally { + connectionLock.unlock(); + } + } + + /** + * Deprecated. + * @deprecated use {@link #reportFailure(Exception)} instead. + */ + @Deprecated + public void reportFailure() { + reportFailure(null); + } + + /** + * Report this synchronization point as failed because of the given exception. The {@code failureException} must be set. + * + * @param failureException the exception causing this synchronization point to fail. + */ + public void reportFailure(E failureException) { + assert failureException != null; + connectionLock.lock(); + try { + state = State.Failure; + this.failureException = failureException; + condition.signalAll(); + } + finally { + connectionLock.unlock(); + } + } + + /** + * 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. + * + * @return true if the synchronization point was successful, false otherwise. + */ + public boolean wasSuccessful() { + connectionLock.lock(); + try { + return state == State.Success; + } + finally { + connectionLock.unlock(); + } + } + + public boolean isNotInInitialState() { + connectionLock.lock(); + try { + return state != State.Initial; + } + finally { + connectionLock.unlock(); + } + } + + /** + * Check if this synchronization point has its request already sent. + * + * @return true if the request was already sent, false otherwise. + */ + public boolean requestSent() { + connectionLock.lock(); + try { + return state == State.RequestSent; + } + finally { + connectionLock.unlock(); + } + } + + public E getFailureException() { + connectionLock.lock(); + try { + return failureException; + } + finally { + connectionLock.unlock(); + } + } + + public void resetTimeout() { + waitStart = System.currentTimeMillis(); + } + + /** + * Wait for the condition to become something else as {@link State#RequestSent} or {@link State#Initial}. + * {@link #reportSuccess()}, {@link #reportFailure()} and {@link #reportFailure(Exception)} will either set this + * synchronization point to {@link State#Success} or {@link State#Failure}. If none of them is set after the + * connections reply timeout, this method will set the state of {@link State#NoResponse}. + * @throws InterruptedException if the calling thread was interrupted. + */ + private void waitForConditionOrTimeout() throws InterruptedException { + waitStart = System.currentTimeMillis(); + while (state == State.RequestSent || state == State.Initial) { + long timeout = connection.getReplyTimeout(); + long remainingWaitMillis = timeout - (System.currentTimeMillis() - waitStart); + long remainingWait = TimeUnit.MILLISECONDS.toNanos(remainingWaitMillis); + + if (remainingWait <= 0) { + state = State.NoResponse; + break; + } + + try { + condition.awaitNanos(remainingWait); + } catch (InterruptedException e) { + state = State.Interrupted; + throw e; + } + } + } + + 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. + *

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

+ * @return true if synchronization point was successful, false on failure. + * @throws NoResponseException if there was no response from the remote entity. + */ + private Exception checkForResponse() throws NoResponseException { + switch (state) { + case Initial: + case NoResponse: + case RequestSent: + throw NoResponseException.newWith(connection, waitFor); + case Success: + return null; + case Failure: + return getException(); + default: + throw new AssertionError("Unknown state " + state); + } + } + + private enum State { + Initial, + RequestSent, + NoResponse, + Success, + Failure, + Interrupted, + } +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/XmppInputOutputFilter.java b/smack-core/src/main/java/org/jivesoftware/smack/XmppInputOutputFilter.java index 97fdff246..18432f564 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/XmppInputOutputFilter.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/XmppInputOutputFilter.java @@ -67,8 +67,7 @@ public interface XmppInputOutputFilter { default void closeInputOutput() { } - default void waitUntilInputOutputClosed() throws IOException, NoResponseException, CertificateException, - InterruptedException, SmackException, XMPPException { + default void waitUntilInputOutputClosed() throws IOException, NoResponseException, CertificateException, InterruptedException, SmackException { } Object getStats(); diff --git a/smack-core/src/main/java/org/jivesoftware/smack/c2s/ModularXmppClientToServerConnection.java b/smack-core/src/main/java/org/jivesoftware/smack/c2s/ModularXmppClientToServerConnection.java index f62caf8ee..061d25e7a 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/c2s/ModularXmppClientToServerConnection.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/c2s/ModularXmppClientToServerConnection.java @@ -35,6 +35,7 @@ import javax.net.ssl.SSLSession; import org.jivesoftware.smack.AbstractXMPPConnection; import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.SmackException.ConnectionUnexpectedTerminatedException; import org.jivesoftware.smack.SmackException.NoResponseException; import org.jivesoftware.smack.SmackException.NotConnectedException; import org.jivesoftware.smack.SmackFuture; @@ -77,7 +78,6 @@ import org.jivesoftware.smack.util.ArrayBlockingQueueWithShutdown; import org.jivesoftware.smack.util.ExtendedAppendable; import org.jivesoftware.smack.util.PacketParserUtils; import org.jivesoftware.smack.util.StringUtils; -import org.jivesoftware.smack.util.Supplier; import org.jivesoftware.smack.xml.XmlPullParser; import org.jivesoftware.smack.xml.XmlPullParserException; @@ -142,8 +142,7 @@ public final class ModularXmppClientToServerConnection extends AbstractXMPPConne @Override public void onStreamClosed() { - ModularXmppClientToServerConnection.this.closingStreamReceived = true; - notifyWaitingThreads(); + ModularXmppClientToServerConnection.this.closingStreamReceived.reportSuccess(); } @Override @@ -178,7 +177,7 @@ public final class ModularXmppClientToServerConnection extends AbstractXMPPConne @Override public void newStreamOpenWaitForFeaturesSequence(String waitFor) throws InterruptedException, - SmackException, XMPPException { + ConnectionUnexpectedTerminatedException, NoResponseException, NotConnectedException { ModularXmppClientToServerConnection.this.newStreamOpenWaitForFeaturesSequence(waitFor); } @@ -199,14 +198,8 @@ public final class ModularXmppClientToServerConnection extends AbstractXMPPConne } @Override - public void waitForCondition(Supplier condition, String waitFor) - throws InterruptedException, SmackException, XMPPException { - ModularXmppClientToServerConnection.this.waitForConditionOrThrowConnectionException(condition, waitFor); - } - - @Override - public void notifyWaitingThreads() { - ModularXmppClientToServerConnection.this.notifyWaitingThreads(); + public Exception getCurrentConnectionException() { + return ModularXmppClientToServerConnection.this.currentConnectionException; } @Override @@ -270,13 +263,14 @@ public final class ModularXmppClientToServerConnection extends AbstractXMPPConne revertedState.resetState(); } - protected void walkStateGraph(WalkStateGraphContext walkStateGraphContext) - throws XMPPException, IOException, SmackException, InterruptedException { + protected void walkStateGraph(WalkStateGraphContext walkStateGraphContext) throws XMPPErrorException, + SASLErrorException, FailedNonzaException, IOException, SmackException, InterruptedException { // Save a copy of the current state GraphVertex previousStateVertex = currentStateVertex; try { walkStateGraphInternal(walkStateGraphContext); - } catch (IOException | SmackException | InterruptedException | XMPPException e) { + } catch (XMPPErrorException | SASLErrorException | FailedNonzaException | IOException | SmackException + | InterruptedException e) { currentStateVertex = previousStateVertex; // Unwind the state. State revertedState = currentStateVertex.getElement(); @@ -285,8 +279,8 @@ public final class ModularXmppClientToServerConnection extends AbstractXMPPConne } } - private void walkStateGraphInternal(WalkStateGraphContext walkStateGraphContext) - throws IOException, SmackException, InterruptedException, XMPPException { + private void walkStateGraphInternal(WalkStateGraphContext walkStateGraphContext) throws XMPPErrorException, + SASLErrorException, IOException, SmackException, InterruptedException, FailedNonzaException { // Save a copy of the current state final GraphVertex initialStateVertex = currentStateVertex; final State initialState = initialStateVertex.getElement(); @@ -359,19 +353,21 @@ public final class ModularXmppClientToServerConnection extends AbstractXMPPConne } /** - * Attempt to enter a state. Note that this method may return null if this state can be safely ignored. + * Attempt to enter a state. Note that this method may return null if this state can be safely ignored ignored. * * @param successorStateVertex the successor state vertex. * @param walkStateGraphContext the "walk state graph" context. * @return A state transition result or null if this state can be ignored. * @throws SmackException if Smack detected an exceptional situation. - * @throws XMPPException if an XMPP protocol error was received. + * @throws XMPPErrorException if an XMPP protocol error was received. + * @throws SASLErrorException if a SASL protocol error was returned. * @throws IOException if an I/O error occurred. * @throws InterruptedException if the calling thread was interrupted. + * @throws FailedNonzaException if an XMPP protocol failure was received. */ private StateTransitionResult attemptEnterState(GraphVertex successorStateVertex, - WalkStateGraphContext walkStateGraphContext) throws SmackException, XMPPException, - IOException, InterruptedException { + WalkStateGraphContext walkStateGraphContext) throws SmackException, XMPPErrorException, + SASLErrorException, IOException, InterruptedException, FailedNonzaException { final GraphVertex initialStateVertex = currentStateVertex; final State initialState = initialStateVertex.getElement(); final State successorState = successorStateVertex.getElement(); @@ -404,7 +400,8 @@ public final class ModularXmppClientToServerConnection extends AbstractXMPPConne invokeConnectionStateMachineListener(new ConnectionStateEvent.AboutToTransitionInto(initialState, successorState)); transitionAttemptResult = successorState.transitionInto(walkStateGraphContext); - } catch (SmackException | IOException | InterruptedException | XMPPException e) { + } catch (SmackException | XMPPErrorException | SASLErrorException | IOException | InterruptedException + | FailedNonzaException e) { // Unwind the state here too, since this state will not be unwound by walkStateGraph(), as it will not // become a predecessor state in the walk. unwindState(successorState); @@ -477,7 +474,8 @@ public final class ModularXmppClientToServerConnection extends AbstractXMPPConne try { walkStateGraph(context); - } catch (IOException | SmackException | InterruptedException | XMPPException e) { + } catch (XMPPErrorException | SASLErrorException | IOException | SmackException | InterruptedException + | FailedNonzaException e) { throw new IllegalStateException("A walk to disconnected state should never throw", e); } } @@ -493,7 +491,9 @@ public final class ModularXmppClientToServerConnection extends AbstractXMPPConne @Override protected void afterFeaturesReceived() { featuresReceived = true; - notifyWaitingThreads(); + synchronized (this) { + notifyAll(); + } } protected void parseAndProcessElement(String element) { @@ -522,10 +522,8 @@ public final class ModularXmppClientToServerConnection extends AbstractXMPPConne break; case "error": StreamError streamError = PacketParserUtils.parseStreamError(parser, null); - StreamErrorException streamErrorException = new StreamErrorException(streamError); - currentXmppException = streamErrorException; - notifyWaitingThreads(); - throw streamErrorException; + saslFeatureReceived.reportFailure(new StreamErrorException(streamError)); + throw new StreamErrorException(streamError); case "features": parseFeatures(parser); afterFeaturesReceived(); @@ -552,12 +550,25 @@ public final class ModularXmppClientToServerConnection extends AbstractXMPPConne } protected void waitForFeaturesReceived(String waitFor) - throws InterruptedException, SmackException, XMPPException { - waitForConditionOrThrowConnectionException(() -> featuresReceived, waitFor); + throws InterruptedException, ConnectionUnexpectedTerminatedException, NoResponseException { + long waitStartMs = System.currentTimeMillis(); + long timeoutMs = getReplyTimeout(); + synchronized (this) { + while (!featuresReceived && currentConnectionException == null) { + long remainingWaitMs = timeoutMs - (System.currentTimeMillis() - waitStartMs); + if (remainingWaitMs <= 0) { + throw NoResponseException.newWith(this, waitFor); + } + wait(remainingWaitMs); + } + if (currentConnectionException != null) { + throw new SmackException.ConnectionUnexpectedTerminatedException(currentConnectionException); + } + } } protected void newStreamOpenWaitForFeaturesSequence(String waitFor) throws InterruptedException, - SmackException, XMPPException { + ConnectionUnexpectedTerminatedException, NoResponseException, NotConnectedException { prepareToWaitForFeaturesReceived(); sendStreamOpen(); waitForFeaturesReceived(waitFor); @@ -752,7 +763,8 @@ public final class ModularXmppClientToServerConnection extends AbstractXMPPConne @Override public StateTransitionResult.AttemptResult transitionInto(WalkStateGraphContext walkStateGraphContext) - throws IOException, SmackException, InterruptedException, XMPPException { + throws XMPPErrorException, SASLErrorException, IOException, SmackException, + InterruptedException { prepareToWaitForFeaturesReceived(); LoginContext loginContext = walkStateGraphContext.getLoginContext(); @@ -801,12 +813,12 @@ public final class ModularXmppClientToServerConnection extends AbstractXMPPConne @Override public StateTransitionResult.AttemptResult transitionInto(WalkStateGraphContext walkStateGraphContext) - throws IOException, SmackException, InterruptedException, XMPPException { + throws XMPPErrorException, SASLErrorException, IOException, SmackException, + InterruptedException { // Calling bindResourceAndEstablishSession() below requires the lastFeaturesReceived sync point to be signaled. // Since we entered this state, the FSM has decided that the last features have been received, hence signal // the sync point. - lastFeaturesReceived = true; - notifyWaitingThreads(); + lastFeaturesReceived.reportSuccess(); LoginContext loginContext = walkStateGraphContext.getLoginContext(); Resourcepart resource = bindResourceAndEstablishSession(loginContext.resource); @@ -902,7 +914,7 @@ public final class ModularXmppClientToServerConnection extends AbstractXMPPConne @Override public StateTransitionResult.AttemptResult transitionInto(WalkStateGraphContext walkStateGraphContext) { - closingStreamReceived = false; + closingStreamReceived.init(); boolean streamCloseIssued = outgoingElementsQueue.offerAndShutdown(StreamClose.INSTANCE); @@ -924,7 +936,7 @@ public final class ModularXmppClientToServerConnection extends AbstractXMPPConne XmppInputOutputFilter filter = it.next(); try { filter.waitUntilInputOutputClosed(); - } catch (IOException | CertificateException | InterruptedException | SmackException | XMPPException e) { + } catch (IOException | CertificateException | InterruptedException | SmackException e) { LOGGER.log(Level.WARNING, "waitUntilInputOutputClosed() threw", e); } } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/c2s/internal/ModularXmppClientToServerConnectionInternal.java b/smack-core/src/main/java/org/jivesoftware/smack/c2s/internal/ModularXmppClientToServerConnectionInternal.java index 81a485771..b08676914 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/c2s/internal/ModularXmppClientToServerConnectionInternal.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/c2s/internal/ModularXmppClientToServerConnectionInternal.java @@ -22,12 +22,11 @@ import java.nio.channels.SelectionKey; import java.util.ListIterator; import java.util.Queue; -import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.SmackException.ConnectionUnexpectedTerminatedException; import org.jivesoftware.smack.SmackException.NoResponseException; import org.jivesoftware.smack.SmackException.NotConnectedException; import org.jivesoftware.smack.SmackReactor; import org.jivesoftware.smack.SmackReactor.ChannelSelectedCallback; -import org.jivesoftware.smack.XMPPException; import org.jivesoftware.smack.XMPPException.FailedNonzaException; import org.jivesoftware.smack.XmppInputOutputFilter; import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnection; @@ -39,7 +38,6 @@ import org.jivesoftware.smack.packet.Nonza; import org.jivesoftware.smack.packet.TopLevelStreamElement; import org.jivesoftware.smack.packet.XmlEnvironment; import org.jivesoftware.smack.util.Consumer; -import org.jivesoftware.smack.util.Supplier; import org.jivesoftware.smack.xml.XmlPullParser; public abstract class ModularXmppClientToServerConnectionInternal { @@ -100,7 +98,7 @@ public abstract class ModularXmppClientToServerConnectionInternal { public abstract ListIterator getXmppInputOutputFilterEndIterator(); public abstract void newStreamOpenWaitForFeaturesSequence(String waitFor) throws InterruptedException, - NoResponseException, NotConnectedException, SmackException, XMPPException; + ConnectionUnexpectedTerminatedException, NoResponseException, NotConnectedException; public abstract SmackTlsContext getSmackTlsContext(); @@ -110,9 +108,7 @@ public abstract class ModularXmppClientToServerConnectionInternal { public abstract void asyncGo(Runnable runnable); - public abstract void waitForCondition(Supplier condition, String waitFor) throws InterruptedException, SmackException, XMPPException; - - public abstract void notifyWaitingThreads(); + public abstract Exception getCurrentConnectionException(); public abstract void setCompressionEnabled(boolean compressionEnabled); diff --git a/smack-core/src/main/java/org/jivesoftware/smack/compress/packet/Compressed.java b/smack-core/src/main/java/org/jivesoftware/smack/compress/packet/Compressed.java index e0278cff4..22026a161 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/compress/packet/Compressed.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/compress/packet/Compressed.java @@ -1,6 +1,6 @@ /** * - * Copyright © 2014-2020 Florian Schmaus + * Copyright © 2014-2015 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,15 +16,12 @@ */ package org.jivesoftware.smack.compress.packet; -import javax.xml.namespace.QName; - import org.jivesoftware.smack.packet.Nonza; public final class Compressed implements Nonza { public static final String ELEMENT = "compressed"; public static final String NAMESPACE = Compress.NAMESPACE; - public static final QName QNAME = new QName(NAMESPACE, ELEMENT); public static final Compressed INSTANCE = new Compressed(); diff --git a/smack-core/src/main/java/org/jivesoftware/smack/compress/packet/Failure.java b/smack-core/src/main/java/org/jivesoftware/smack/compress/packet/Failure.java index 2d409db65..473abaccf 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/compress/packet/Failure.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/compress/packet/Failure.java @@ -1,6 +1,6 @@ /** * - * Copyright 2018-2020 Florian Schmaus + * 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. @@ -18,8 +18,6 @@ package org.jivesoftware.smack.compress.packet; import java.util.Objects; -import javax.xml.namespace.QName; - import org.jivesoftware.smack.packet.Nonza; import org.jivesoftware.smack.packet.StanzaError; import org.jivesoftware.smack.util.XmlStringBuilder; @@ -28,7 +26,6 @@ public class Failure implements Nonza { public static final String ELEMENT = "failure"; public static final String NAMESPACE = Compress.NAMESPACE; - public static final QName QNAME = new QName(NAMESPACE, ELEMENT); public enum CompressFailureError { setup_failed, diff --git a/smack-core/src/main/java/org/jivesoftware/smack/compression/CompressionModule.java b/smack-core/src/main/java/org/jivesoftware/smack/compression/CompressionModule.java index ce0dddeea..2a018899b 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/compression/CompressionModule.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/compression/CompressionModule.java @@ -17,8 +17,10 @@ package org.jivesoftware.smack.compression; import org.jivesoftware.smack.ConnectionConfiguration; -import org.jivesoftware.smack.SmackException; -import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.SmackException.ConnectionUnexpectedTerminatedException; +import org.jivesoftware.smack.SmackException.NoResponseException; +import org.jivesoftware.smack.SmackException.NotConnectedException; +import org.jivesoftware.smack.XMPPException.FailedNonzaException; import org.jivesoftware.smack.XmppInputOutputFilter; import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnection.AuthenticatedButUnboundStateDescriptor; import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnection.ResourceBindingStateDescriptor; @@ -88,7 +90,8 @@ public class CompressionModule extends ModularXmppClientToServerConnectionModule @Override public StateTransitionResult.AttemptResult transitionInto(WalkStateGraphContext walkStateGraphContext) - throws InterruptedException, SmackException, XMPPException { + throws NoResponseException, NotConnectedException, FailedNonzaException, InterruptedException, + ConnectionUnexpectedTerminatedException { final String compressionMethod = selectedCompressionFactory.getCompressionMethod(); connectionInternal.sendAndWaitForResponse(new Compress(compressionMethod), Compressed.class, Failure.class); diff --git a/smack-core/src/main/java/org/jivesoftware/smack/fsm/State.java b/smack-core/src/main/java/org/jivesoftware/smack/fsm/State.java index a4a7b76ea..f8749161d 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/fsm/State.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/fsm/State.java @@ -19,9 +19,11 @@ package org.jivesoftware.smack.fsm; import java.io.IOException; import org.jivesoftware.smack.SmackException; -import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.XMPPException.FailedNonzaException; +import org.jivesoftware.smack.XMPPException.XMPPErrorException; import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal; import org.jivesoftware.smack.c2s.internal.WalkStateGraphContext; +import org.jivesoftware.smack.sasl.SASLErrorException; /** * Note that this is an non-static inner class of XmppClientToServerConnection so that states can inspect and modify @@ -51,7 +53,8 @@ public abstract class State { } public abstract StateTransitionResult.AttemptResult transitionInto(WalkStateGraphContext walkStateGraphContext) - throws IOException, SmackException, InterruptedException, XMPPException; + throws XMPPErrorException, SASLErrorException, IOException, SmackException, + InterruptedException, FailedNonzaException; public StateDescriptor getStateDescriptor() { return stateDescriptor; diff --git a/smack-core/src/main/java/org/jivesoftware/smack/fsm/StateDescriptor.java b/smack-core/src/main/java/org/jivesoftware/smack/fsm/StateDescriptor.java index aec14b18e..0ef98885c 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/fsm/StateDescriptor.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/fsm/StateDescriptor.java @@ -212,7 +212,7 @@ public abstract class StateDescriptor { protected State constructState(ModularXmppClientToServerConnectionInternal connectionInternal) { ModularXmppClientToServerConnection connection = connectionInternal.connection; try { - // If stateClassConstructor is null here, then you probably forgot to override the + // If stateClassConstructor is null here, then you probably forgot to override the the // StateDescriptor.constructState() method? return stateClassConstructor.newInstance(connection, this, connectionInternal); } catch (InstantiationException | IllegalAccessException | IllegalArgumentException diff --git a/smack-core/src/main/java/org/jivesoftware/smack/fsm/StateTransitionResult.java b/smack-core/src/main/java/org/jivesoftware/smack/fsm/StateTransitionResult.java index a9acb4f32..053d35e77 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/fsm/StateTransitionResult.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/fsm/StateTransitionResult.java @@ -67,14 +67,6 @@ public abstract class StateTransitionResult { } } - public static final class FailureCausedByTimeout extends Failure { - - public FailureCausedByTimeout(String failureMessage) { - super(failureMessage); - } - - } - public abstract static class TransitionImpossible extends StateTransitionResult { protected TransitionImpossible(String message) { super(message); diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/Message.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/Message.java index a85f7c5b6..9b2d392a0 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/packet/Message.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/Message.java @@ -17,16 +17,20 @@ package org.jivesoftware.smack.packet; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Locale; +import java.util.Set; import javax.xml.namespace.QName; -import org.jivesoftware.smack.XMPPConnection; import org.jivesoftware.smack.util.EqualsUtil; import org.jivesoftware.smack.util.HashCode; import org.jivesoftware.smack.util.Objects; import org.jivesoftware.smack.util.StringUtils; +import org.jivesoftware.smack.util.TypedCloneable; import org.jivesoftware.smack.util.XmlStringBuilder; import org.jxmpp.jid.Jid; @@ -58,7 +62,7 @@ import org.jxmpp.stringprep.XmppStringprepException; * @author Matt Tucker */ public final class Message extends MessageOrPresence - implements MessageView { + implements MessageView, TypedCloneable { public static final String ELEMENT = "message"; public static final String BODY = "body"; @@ -182,6 +186,59 @@ public final class Message extends MessageOrPresence this.type = type; } + /** + * Returns the default subject of the message, or null if the subject has not been set. + * The subject is a short description of message contents. + *

+ * The default subject of a message is the subject that corresponds to the message's language. + * (see {@link #getLanguage()}) or if no language is set to the applications default + * language (see {@link Stanza#getDefaultLanguage()}). + * + * @return the subject of the message. + */ + public String getSubject() { + return getSubject(null); + } + + /** + * Returns the subject corresponding to the language. If the language is null, the method result + * will be the same as {@link #getSubject()}. Null will be returned if the language does not have + * a corresponding subject. + * + * @param language the language of the subject to return. + * @return the subject related to the passed in language. + */ + public String getSubject(String language) { + Subject subject = getMessageSubject(language); + return subject == null ? null : subject.subject; + } + + private Subject getMessageSubject(String language) { + language = determineLanguage(language); + for (Subject subject : getSubjects()) { + if (Objects.equals(language, subject.language) + || (subject.language == null && Objects.equals(this.language, language))) { + return subject; + } + } + return null; + } + + /** + * Returns a set of all subjects in this Message, including the default message subject accessible + * from {@link #getSubject()}. + * + * @return a collection of all subjects in this message. + */ + public Set getSubjects() { + List subjectList = getExtensions(Subject.class); + + Set subjects = new HashSet<>(subjectList.size()); + subjects.addAll(subjectList); + + return subjects; + } + /** * Sets the subject of the message. The subject is a short description of * message contents. @@ -210,7 +267,7 @@ public final class Message extends MessageOrPresence @Deprecated // TODO: Remove when stanza builder is ready. public Subject addSubject(String language, String subject) { - language = Stanza.determineLanguage(this, language); + language = determineLanguage(language); List currentSubjects = getExtensions(Subject.class); for (Subject currentSubject : currentSubjects) { @@ -233,7 +290,7 @@ public final class Message extends MessageOrPresence @Deprecated // TODO: Remove when stanza builder is ready. public boolean removeSubject(String language) { - language = Stanza.determineLanguage(this, language); + language = determineLanguage(language); for (Subject subject : getExtensions(Subject.class)) { if (language.equals(subject.language)) { return removeSubject(subject); @@ -254,6 +311,77 @@ public final class Message extends MessageOrPresence return removeExtension(subject) != null; } + /** + * Returns all the languages being used for the subjects, not including the default subject. + * + * @return the languages being used for the subjects. + */ + public List getSubjectLanguages() { + Subject defaultSubject = getMessageSubject(null); + List languages = new ArrayList(); + for (Subject subject : getExtensions(Subject.class)) { + if (!subject.equals(defaultSubject)) { + languages.add(subject.language); + } + } + return Collections.unmodifiableList(languages); + } + + /** + * Returns the default body of the message, or null if the body has not been set. The body + * is the main message contents. + *

+ * The default body of a message is the body that corresponds to the message's language. + * (see {@link #getLanguage()}) or if no language is set to the applications default + * language (see {@link Stanza#getDefaultLanguage()}). + * + * @return the body of the message. + */ + public String getBody() { + return getBody(language); + } + + /** + * Returns the body corresponding to the language. If the language is null, the method result + * will be the same as {@link #getBody()}. Null will be returned if the language does not have + * a corresponding body. + * + * @param language the language of the body to return. + * @return the body related to the passed in language. + * @since 3.0.2 + */ + public String getBody(String language) { + Body body = getMessageBody(language); + return body == null ? null : body.message; + } + + private Body getMessageBody(String language) { + language = determineLanguage(language); + for (Body body : getBodies()) { + if (Objects.equals(language, body.language) || (language != null && language.equals(this.language) && body.language == null)) { + return body; + } + } + return null; + } + + /** + * Returns a set of all bodies in this Message, including the default message body accessible + * from {@link #getBody()}. + * + * @return a collection of all bodies in this Message. + * @since 3.0.2 + */ + public Set getBodies() { + List bodiesList = getExtensions(Body.ELEMENT, Body.NAMESPACE); + Set resultSet = new HashSet<>(bodiesList.size()); + for (ExtensionElement extensionElement : bodiesList) { + Body body = (Body) extensionElement; + resultSet.add(body); + } + return resultSet; + } + /** * Sets the body of the message. * @@ -303,7 +431,7 @@ public final class Message extends MessageOrPresence @Deprecated // TODO: Remove when stanza builder is ready. public Body addBody(String language, String body) { - language = Stanza.determineLanguage(this, language); + language = determineLanguage(language); removeBody(language); @@ -322,7 +450,7 @@ public final class Message extends MessageOrPresence @Deprecated // TODO: Remove when stanza builder is ready. public boolean removeBody(String language) { - language = Stanza.determineLanguage(this, language); + language = determineLanguage(language); for (Body body : getBodies()) { String bodyLanguage = body.getLanguage(); if (Objects.equals(bodyLanguage, language)) { @@ -348,6 +476,37 @@ public final class Message extends MessageOrPresence return removedElement != null; } + /** + * Returns all the languages being used for the bodies, not including the default body. + * + * @return the languages being used for the bodies. + * @since 3.0.2 + */ + public List getBodyLanguages() { + Body defaultBody = getMessageBody(null); + List languages = new ArrayList(); + for (Body body : getBodies()) { + if (!body.equals(defaultBody)) { + languages.add(body.language); + } + } + return Collections.unmodifiableList(languages); + } + + /** + * Returns the thread id of the message, which is a unique identifier for a sequence + * of "chat" messages. If no thread id is set, null will be returned. + * + * @return the thread id of the message, or null if it doesn't exist. + */ + public String getThread() { + Message.Thread thread = getExtension(Message.Thread.class); + if (thread == null) { + return null; + } + return thread.getThread(); + } + /** * Sets the thread id of the message, which is a unique identifier for a sequence * of "chat" messages. @@ -361,6 +520,18 @@ public final class Message extends MessageOrPresence addExtension(new Message.Thread(thread)); } + private String determineLanguage(String language) { + + // empty string is passed by #setSubject() and #setBody() and is the same as null + language = "".equals(language) ? null : language; + + // if given language is null check if message language is set + if (language == null && this.language != null) { + return this.language; + } + return language; + } + @Override public String getElementName() { return ELEMENT; @@ -371,16 +542,6 @@ public final class Message extends MessageOrPresence return StanzaBuilder.buildMessageFrom(this, getStanzaId()); } - @Override - public MessageBuilder asBuilder(String id) { - return StanzaBuilder.buildMessageFrom(this, id); - } - - @Override - public MessageBuilder asBuilder(XMPPConnection connection) { - return connection.getStanzaFactory().buildMessageStanzaFrom(this); - } - @Override public String toString() { StringBuilder sb = new StringBuilder(); @@ -419,10 +580,7 @@ public final class Message extends MessageOrPresence * instance. *

* @return a clone of this message. - * @deprecated use {@link #asBuilder()} instead. */ - // TODO: Remove in Smack 4.5. - @Deprecated @Override public Message clone() { return new Message(this); diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/MessageOrPresence.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/MessageOrPresence.java index f48b794a0..c885e941c 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/packet/MessageOrPresence.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/MessageOrPresence.java @@ -1,6 +1,6 @@ /** * - * Copyright 2019-2020 Florian Schmaus + * Copyright 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. @@ -16,8 +16,6 @@ */ package org.jivesoftware.smack.packet; -import org.jivesoftware.smack.XMPPConnection; - public abstract class MessageOrPresence> extends Stanza { @Deprecated @@ -35,8 +33,4 @@ public abstract class MessageOrPresence - * The default subject of a message is the subject that corresponds to the message's language. - * (see {@link #getLanguage()}) or if no language is set to the applications default - * language (see {@link Stanza#getDefaultLanguage()}). - * - * @return the subject of the message. - */ - default String getSubject() { - return getSubject(null); - } - - /** - * Returns the subject corresponding to the language. If the language is null, the method result - * will be the same as {@link #getSubject()}. Null will be returned if the language does not have - * a corresponding subject. - * - * @param language the language of the subject to return. - * @return the subject related to the passed in language. - */ - default String getSubject(String language) { - Subject subject = getMessageSubject(language); - return subject == null ? null : subject.getSubject(); - } - - default Message.Subject getMessageSubject(String language) { - language = Stanza.determineLanguage(this, language); - for (Message.Subject subject : getSubjects()) { - if (Objects.equals(language, subject.getLanguage()) - || (subject.getLanguage() == null && Objects.equals(getLanguage(), language))) { - return subject; - } - } - return null; - } - - /** - * Returns a set of all subjects in this Message, including the default message subject accessible - * from {@link #getSubject()}. - * - * @return a collection of all subjects in this message. - */ - default Set getSubjects() { - List subjectList = getExtensions(Subject.class); - - Set subjects = new HashSet<>(subjectList.size()); - subjects.addAll(subjectList); - - return subjects; - } - - /** - * Returns all the languages being used for the subjects, not including the default subject. - * - * @return the languages being used for the subjects. - */ - default List getSubjectLanguages() { - Message.Subject defaultSubject = getMessageSubject(null); - List languages = new ArrayList(); - for (Message.Subject subject : getExtensions(Message.Subject.class)) { - if (!subject.equals(defaultSubject)) { - languages.add(subject.getLanguage()); - } - } - return Collections.unmodifiableList(languages); - } - - /** - * Returns the default body of the message, or null if the body has not been set. The body - * is the main message contents. - *

- * The default body of a message is the body that corresponds to the message's language. - * (see {@link #getLanguage()}) or if no language is set to the applications default - * language (see {@link Stanza#getDefaultLanguage()}). - * - * @return the body of the message. - */ - default String getBody() { - return getBody(getLanguage()); - } - - /** - * Returns the body corresponding to the language. If the language is null, the method result - * will be the same as {@link #getBody()}. Null will be returned if the language does not have - * a corresponding body. - * - * @param language the language of the body to return. - * @return the body related to the passed in language. - * @since 3.0.2 - */ - default String getBody(String language) { - Message.Body body = getMessageBody(language); - return body == null ? null : body.getMessage(); - } - - default Message.Body getMessageBody(String language) { - language = Stanza.determineLanguage(this, language); - for (Message.Body body : getBodies()) { - if (Objects.equals(language, body.getLanguage()) || (language != null && language.equals(getLanguage()) && body.getLanguage() == null)) { - return body; - } - } - return null; - } - - /** - * Returns a set of all bodies in this Message, including the default message body accessible - * from {@link #getBody()}. - * - * @return a collection of all bodies in this Message. - * @since 3.0.2 - */ - default Set getBodies() { - List bodiesList = getExtensions(Message.Body.QNAME); - Set resultSet = new HashSet<>(bodiesList.size()); - for (ExtensionElement extensionElement : bodiesList) { - Message.Body body = (Message.Body) extensionElement; - resultSet.add(body); - } - return resultSet; - } - - /** - * Returns all the languages being used for the bodies, not including the default body. - * - * @return the languages being used for the bodies. - * @since 3.0.2 - */ - default List getBodyLanguages() { - Message.Body defaultBody = getMessageBody(null); - List languages = new ArrayList(); - for (Message.Body body : getBodies()) { - if (!body.equals(defaultBody)) { - languages.add(body.getLanguage()); - } - } - return Collections.unmodifiableList(languages); - } - - /** - * Returns the thread id of the message, which is a unique identifier for a sequence - * of "chat" messages. If no thread id is set, null will be returned. - * - * @return the thread id of the message, or null if it doesn't exist. - */ - default String getThread() { - Message.Thread thread = getExtension(Message.Thread.class); - if (thread == null) { - return null; - } - return thread.getThread(); - } } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/Presence.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/Presence.java index 0c7014110..0f819b429 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/packet/Presence.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/Presence.java @@ -20,9 +20,9 @@ package org.jivesoftware.smack.packet; import java.util.List; import java.util.Locale; -import org.jivesoftware.smack.XMPPConnection; import org.jivesoftware.smack.util.Objects; import org.jivesoftware.smack.util.StringUtils; +import org.jivesoftware.smack.util.TypedCloneable; import org.jivesoftware.smack.util.XmlStringBuilder; import org.jxmpp.jid.Jid; @@ -61,7 +61,7 @@ import org.jxmpp.jid.Jid; * @author Matt Tucker */ public final class Presence extends MessageOrPresence - implements PresenceView { + implements PresenceView, TypedCloneable { public static final String ELEMENT = "presence"; @@ -282,16 +282,6 @@ public final class Presence extends MessageOrPresence return StanzaBuilder.buildPresenceFrom(this, getStanzaId()); } - @Override - public PresenceBuilder asBuilder(String id) { - return StanzaBuilder.buildPresenceFrom(this, id); - } - - @Override - public PresenceBuilder asBuilder(XMPPConnection connection) { - return connection.getStanzaFactory().buildPresenceStanzaFrom(this); - } - @Override public String toString() { StringBuilder sb = new StringBuilder(); @@ -353,10 +343,7 @@ public final class Presence extends MessageOrPresence * instance. *

* @return a clone of this presence. - * @deprecated use {@link #asBuilder()} instead. */ - // TODO: Remove in Smack 4.5. - @Deprecated @Override public Presence clone() { return new Presence(this); @@ -367,10 +354,7 @@ public final class Presence extends MessageOrPresence * * @return a "clone" of this presence with a different stanza ID. * @since 4.1.2 - * @deprecated use {@link #asBuilder(XMPPConnection)} or {@link #asBuilder(String)}instead. */ - // TODO: Remove in Smack 4.5. - @Deprecated public Presence cloneWithNewId() { Presence clone = clone(); clone.setNewStanzaId(); diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/Stanza.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/Stanza.java index bce5ba0f5..ee9377d1a 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/packet/Stanza.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/Stanza.java @@ -575,18 +575,4 @@ public abstract class Stanza implements StanzaView, TopLevelStreamElement { xml.append(error); } } - - /** - * Return the provided non-empty language, or use this {@link XmlLangElement} language (if set). - * - * @param language the provided language, may be the empty string or null. - * @return the provided language or this element's language (if set). - */ - static String determineLanguage(XmlLangElement xmlLangElement, String language) { - if (language != null && !language.isEmpty()) { - return language; - } - - return xmlLangElement.getLanguage(); - } } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/TlsFailure.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/TlsFailure.java index bce64aa05..05aa4b6fa 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/packet/TlsFailure.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/TlsFailure.java @@ -1,6 +1,6 @@ /** * - * Copyright 2018-2020 Florian Schmaus + * 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. @@ -16,15 +16,12 @@ */ package org.jivesoftware.smack.packet; -import javax.xml.namespace.QName; - public final class TlsFailure implements Nonza { public static final TlsFailure INSTANCE = new TlsFailure(); public static final String ELEMENT = "failure"; public static final String NAMESPACE = TlsProceed.NAMESPACE; - public static final QName QNAME = new QName(NAMESPACE, ELEMENT); private TlsFailure() { } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/TlsProceed.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/TlsProceed.java index cf26f3da1..7967a89b7 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/packet/TlsProceed.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/TlsProceed.java @@ -1,6 +1,6 @@ /** * - * Copyright 2018-2020 Florian Schmaus + * 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. @@ -16,15 +16,12 @@ */ package org.jivesoftware.smack.packet; -import javax.xml.namespace.QName; - public final class TlsProceed implements Nonza { public static final TlsProceed INSTANCE = new TlsProceed(); public static final String ELEMENT = "proceed"; public static final String NAMESPACE = "urn:ietf:params:xml:ns:xmpp-tls"; - public static final QName QNAME = new QName(NAMESPACE, ELEMENT); private TlsProceed() { } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/MultiMap.java b/smack-core/src/main/java/org/jivesoftware/smack/util/MultiMap.java index f7dcad596..daa3241a8 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/MultiMap.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/MultiMap.java @@ -1,6 +1,6 @@ /** * - * Copyright © 2015-2020 Florian Schmaus + * Copyright © 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. @@ -34,7 +34,7 @@ import java.util.Set; * @param the type of the keys the map uses. * @param the type of the values the map uses. */ -public class MultiMap { +public class MultiMap implements TypedCloneable> { /** * The constant value {@value}. diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/Supplier.java b/smack-core/src/main/java/org/jivesoftware/smack/util/TypedCloneable.java similarity index 61% rename from smack-core/src/main/java/org/jivesoftware/smack/util/Supplier.java rename to smack-core/src/main/java/org/jivesoftware/smack/util/TypedCloneable.java index f0910adb7..0a2121845 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/Supplier.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/TypedCloneable.java @@ -1,6 +1,6 @@ /** * - * Copyright 2020 Florian Schmaus + * Copyright 2015 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,19 @@ */ package org.jivesoftware.smack.util; -// TODO: Replace with java.util.function.Supplier once Smack's minimum Android SDK level is 24 or higher. -public interface Supplier { +/** + * An extended version of {@link java.lang.Cloneable}, which defines a generic {@link #clone()} + * method. + * + * @param the type returned by {@link #clone()}. + */ +public interface TypedCloneable extends Cloneable { - T get(); + /** + * Clone this instance. + * + * @return a cloned version of this instance. + */ + T clone(); } diff --git a/smack-core/src/test/java/org/jivesoftware/smack/compress/packet/FailureTest.java b/smack-core/src/test/java/org/jivesoftware/smack/compress/packet/FailureTest.java index 9e7ab3b1f..cbc5ba88f 100644 --- a/smack-core/src/test/java/org/jivesoftware/smack/compress/packet/FailureTest.java +++ b/smack-core/src/test/java/org/jivesoftware/smack/compress/packet/FailureTest.java @@ -16,7 +16,7 @@ */ package org.jivesoftware.smack.compress.packet; -import static org.jivesoftware.smack.test.util.XmlAssertUtil.assertXmlSimilar; +import static org.jivesoftware.smack.test.util.XmlUnitUtils.assertXmlSimilar; import java.io.IOException; diff --git a/smack-core/src/test/java/org/jivesoftware/smack/packet/MessageTest.java b/smack-core/src/test/java/org/jivesoftware/smack/packet/MessageTest.java index 4c45be4fe..68d8535fa 100644 --- a/smack-core/src/test/java/org/jivesoftware/smack/packet/MessageTest.java +++ b/smack-core/src/test/java/org/jivesoftware/smack/packet/MessageTest.java @@ -16,7 +16,7 @@ */ package org.jivesoftware.smack.packet; -import static org.jivesoftware.smack.test.util.XmlAssertUtil.assertXmlSimilar; +import static org.jivesoftware.smack.test.util.XmlUnitUtils.assertXmlSimilar; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; diff --git a/smack-core/src/test/java/org/jivesoftware/smack/packet/PresenceTest.java b/smack-core/src/test/java/org/jivesoftware/smack/packet/PresenceTest.java index 19bee63ec..863e4b0ff 100644 --- a/smack-core/src/test/java/org/jivesoftware/smack/packet/PresenceTest.java +++ b/smack-core/src/test/java/org/jivesoftware/smack/packet/PresenceTest.java @@ -16,7 +16,7 @@ */ package org.jivesoftware.smack.packet; -import static org.jivesoftware.smack.test.util.XmlAssertUtil.assertXmlSimilar; +import static org.jivesoftware.smack.test.util.XmlUnitUtils.assertXmlSimilar; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; diff --git a/smack-core/src/test/java/org/jivesoftware/smack/provider/SaslProviderTest.java b/smack-core/src/test/java/org/jivesoftware/smack/provider/SaslProviderTest.java index 944a0503a..9252625ed 100644 --- a/smack-core/src/test/java/org/jivesoftware/smack/provider/SaslProviderTest.java +++ b/smack-core/src/test/java/org/jivesoftware/smack/provider/SaslProviderTest.java @@ -16,7 +16,7 @@ */ package org.jivesoftware.smack.provider; -import static org.jivesoftware.smack.test.util.XmlAssertUtil.assertXmlSimilar; +import static org.jivesoftware.smack.test.util.XmlUnitUtils.assertXmlSimilar; import java.io.IOException; diff --git a/smack-core/src/test/java/org/jivesoftware/smack/util/PacketParserUtilsTest.java b/smack-core/src/test/java/org/jivesoftware/smack/util/PacketParserUtilsTest.java index 3ac5215d5..f775a4fcc 100644 --- a/smack-core/src/test/java/org/jivesoftware/smack/util/PacketParserUtilsTest.java +++ b/smack-core/src/test/java/org/jivesoftware/smack/util/PacketParserUtilsTest.java @@ -16,8 +16,8 @@ */ package org.jivesoftware.smack.util; -import static org.jivesoftware.smack.test.util.XmlAssertUtil.assertXmlNotSimilar; -import static org.jivesoftware.smack.test.util.XmlAssertUtil.assertXmlSimilar; +import static org.jivesoftware.smack.test.util.XmlUnitUtils.assertXmlNotSimilar; +import static org.jivesoftware.smack.test.util.XmlUnitUtils.assertXmlSimilar; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; diff --git a/smack-core/src/test/java/org/jivesoftware/smack/util/XmlStringBuilderTest.java b/smack-core/src/test/java/org/jivesoftware/smack/util/XmlStringBuilderTest.java index 425bdf9b3..8436a4dd2 100644 --- a/smack-core/src/test/java/org/jivesoftware/smack/util/XmlStringBuilderTest.java +++ b/smack-core/src/test/java/org/jivesoftware/smack/util/XmlStringBuilderTest.java @@ -18,7 +18,7 @@ package org.jivesoftware.smack.util; import org.jivesoftware.smack.packet.StandardExtensionElement; import org.jivesoftware.smack.packet.XmlEnvironment; -import org.jivesoftware.smack.test.util.XmlAssertUtil; +import org.jivesoftware.smack.test.util.XmlUnitUtils; import org.junit.jupiter.api.Test; @@ -38,10 +38,10 @@ public class XmlStringBuilderTest { String expectedXml = ""; XmlStringBuilder actualXml = outer.toXML(XmlEnvironment.EMPTY); - XmlAssertUtil.assertXmlSimilar(expectedXml, actualXml); + XmlUnitUtils.assertXmlSimilar(expectedXml, actualXml); StringBuilder actualXmlTwo = actualXml.toXML(XmlEnvironment.EMPTY); - XmlAssertUtil.assertXmlSimilar(expectedXml, actualXmlTwo); + XmlUnitUtils.assertXmlSimilar(expectedXml, actualXmlTwo); } } diff --git a/smack-core/src/testFixtures/java/org/jivesoftware/smack/test/util/XmlAssertUtil.java b/smack-core/src/testFixtures/java/org/jivesoftware/smack/test/util/XmlUnitUtils.java similarity index 94% rename from smack-core/src/testFixtures/java/org/jivesoftware/smack/test/util/XmlAssertUtil.java rename to smack-core/src/testFixtures/java/org/jivesoftware/smack/test/util/XmlUnitUtils.java index 185bf4eea..3d9b5ba0f 100644 --- a/smack-core/src/testFixtures/java/org/jivesoftware/smack/test/util/XmlAssertUtil.java +++ b/smack-core/src/testFixtures/java/org/jivesoftware/smack/test/util/XmlUnitUtils.java @@ -1,6 +1,6 @@ /** * - * Copyright 2014-2020 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. @@ -26,7 +26,8 @@ import org.xmlunit.diff.DefaultNodeMatcher; import org.xmlunit.diff.ElementSelectors; import org.xmlunit.input.NormalizedSource; -public class XmlAssertUtil { +// TODO: Rename this class to XmlAssertUtil +public class XmlUnitUtils { public static void assertXmlNotSimilar(CharSequence xmlOne, CharSequence xmlTwo) { normalizedCompare(xmlOne, xmlTwo).areNotSimilar(); diff --git a/smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/SlotCreateTest.java b/smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/SlotCreateTest.java index 26d940e39..3dc287098 100644 --- a/smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/SlotCreateTest.java +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/SlotCreateTest.java @@ -16,7 +16,7 @@ */ package org.jivesoftware.smackx.httpfileupload; -import static org.jivesoftware.smack.test.util.XmlAssertUtil.assertXmlSimilar; +import static org.jivesoftware.smack.test.util.XmlUnitUtils.assertXmlSimilar; import static org.junit.jupiter.api.Assertions.assertEquals; import java.io.IOException; diff --git a/smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/SlotRequestCreateTest.java b/smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/SlotRequestCreateTest.java index 3bf92210e..e30a60c99 100644 --- a/smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/SlotRequestCreateTest.java +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/SlotRequestCreateTest.java @@ -16,7 +16,7 @@ */ package org.jivesoftware.smackx.httpfileupload; -import static org.jivesoftware.smack.test.util.XmlAssertUtil.assertXmlSimilar; +import static org.jivesoftware.smack.test.util.XmlUnitUtils.assertXmlSimilar; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; diff --git a/smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/provider/SlotProviderTest.java b/smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/provider/SlotProviderTest.java index 9aa9699d9..17c85f32d 100644 --- a/smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/provider/SlotProviderTest.java +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/httpfileupload/provider/SlotProviderTest.java @@ -16,7 +16,7 @@ */ package org.jivesoftware.smackx.httpfileupload.provider; -import static org.jivesoftware.smack.test.util.XmlAssertUtil.assertXmlSimilar; +import static org.jivesoftware.smack.test.util.XmlUnitUtils.assertXmlSimilar; import static org.junit.jupiter.api.Assertions.assertEquals; import java.net.MalformedURLException; diff --git a/smack-experimental/src/test/java/org/jivesoftware/smackx/mam/RetrieveFormFieldsTest.java b/smack-experimental/src/test/java/org/jivesoftware/smackx/mam/RetrieveFormFieldsTest.java index f973fa719..87000a6e8 100644 --- a/smack-experimental/src/test/java/org/jivesoftware/smackx/mam/RetrieveFormFieldsTest.java +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/mam/RetrieveFormFieldsTest.java @@ -16,7 +16,7 @@ */ package org.jivesoftware.smackx.mam; -import static org.jivesoftware.smack.test.util.XmlAssertUtil.assertXmlSimilar; +import static org.jivesoftware.smack.test.util.XmlUnitUtils.assertXmlSimilar; import static org.junit.jupiter.api.Assertions.assertEquals; import org.jivesoftware.smack.packet.StreamOpen; diff --git a/smack-experimental/src/test/java/org/jivesoftware/smackx/message_fastening/MessageFasteningElementsTest.java b/smack-experimental/src/test/java/org/jivesoftware/smackx/message_fastening/MessageFasteningElementsTest.java index 8230f974a..7a36752da 100644 --- a/smack-experimental/src/test/java/org/jivesoftware/smackx/message_fastening/MessageFasteningElementsTest.java +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/message_fastening/MessageFasteningElementsTest.java @@ -16,7 +16,7 @@ */ package org.jivesoftware.smackx.message_fastening; -import static org.jivesoftware.smack.test.util.XmlAssertUtil.assertXmlSimilar; +import static org.jivesoftware.smack.test.util.XmlUnitUtils.assertXmlSimilar; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; diff --git a/smack-experimental/src/test/java/org/jivesoftware/smackx/message_markup/MessageMarkupTest.java b/smack-experimental/src/test/java/org/jivesoftware/smackx/message_markup/MessageMarkupTest.java index 63937453e..42275642c 100644 --- a/smack-experimental/src/test/java/org/jivesoftware/smackx/message_markup/MessageMarkupTest.java +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/message_markup/MessageMarkupTest.java @@ -16,7 +16,7 @@ */ package org.jivesoftware.smackx.message_markup; -import static org.jivesoftware.smack.test.util.XmlAssertUtil.assertXmlSimilar; +import static org.jivesoftware.smack.test.util.XmlUnitUtils.assertXmlSimilar; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; diff --git a/smack-experimental/src/test/java/org/jivesoftware/smackx/message_retraction/element/RetractElementTest.java b/smack-experimental/src/test/java/org/jivesoftware/smackx/message_retraction/element/RetractElementTest.java index 1278caed4..3cb8491de 100644 --- a/smack-experimental/src/test/java/org/jivesoftware/smackx/message_retraction/element/RetractElementTest.java +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/message_retraction/element/RetractElementTest.java @@ -16,7 +16,7 @@ */ package org.jivesoftware.smackx.message_retraction.element; -import static org.jivesoftware.smack.test.util.XmlAssertUtil.assertXmlSimilar; +import static org.jivesoftware.smack.test.util.XmlUnitUtils.assertXmlSimilar; import static org.junit.jupiter.api.Assertions.assertNotNull; import java.io.IOException; diff --git a/smack-experimental/src/test/java/org/jivesoftware/smackx/message_retraction/element/RetractedElementTest.java b/smack-experimental/src/test/java/org/jivesoftware/smackx/message_retraction/element/RetractedElementTest.java index ba5acea49..20dcbd650 100644 --- a/smack-experimental/src/test/java/org/jivesoftware/smackx/message_retraction/element/RetractedElementTest.java +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/message_retraction/element/RetractedElementTest.java @@ -16,7 +16,7 @@ */ package org.jivesoftware.smackx.message_retraction.element; -import static org.jivesoftware.smack.test.util.XmlAssertUtil.assertXmlSimilar; +import static org.jivesoftware.smack.test.util.XmlUnitUtils.assertXmlSimilar; import static org.junit.jupiter.api.Assertions.assertNotNull; import java.io.IOException; diff --git a/smack-experimental/src/test/java/org/jivesoftware/smackx/reference/ReferenceTest.java b/smack-experimental/src/test/java/org/jivesoftware/smackx/reference/ReferenceTest.java index 462c80acc..84106e0a6 100644 --- a/smack-experimental/src/test/java/org/jivesoftware/smackx/reference/ReferenceTest.java +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/reference/ReferenceTest.java @@ -16,7 +16,7 @@ */ package org.jivesoftware.smackx.reference; -import static org.jivesoftware.smack.test.util.XmlAssertUtil.assertXmlSimilar; +import static org.jivesoftware.smack.test.util.XmlUnitUtils.assertXmlSimilar; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; 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 a4ccdc6ce..a95e5caac 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 @@ -16,7 +16,7 @@ */ package org.jivesoftware.smackx.sid; -import static org.jivesoftware.smack.test.util.XmlAssertUtil.assertXmlSimilar; +import static org.jivesoftware.smack.test.util.XmlUnitUtils.assertXmlSimilar; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; diff --git a/smack-experimental/src/test/java/org/jivesoftware/smackx/spoiler/SpoilerTest.java b/smack-experimental/src/test/java/org/jivesoftware/smackx/spoiler/SpoilerTest.java index 5a96059c7..165c4be40 100644 --- a/smack-experimental/src/test/java/org/jivesoftware/smackx/spoiler/SpoilerTest.java +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/spoiler/SpoilerTest.java @@ -16,7 +16,7 @@ */ package org.jivesoftware.smackx.spoiler; -import static org.jivesoftware.smack.test.util.XmlAssertUtil.assertXmlSimilar; +import static org.jivesoftware.smack.test.util.XmlUnitUtils.assertXmlSimilar; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; 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 312b559b5..61103d2ee 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 @@ -962,9 +962,8 @@ public final class ServiceDiscoveryManager extends Manager { // to respect ConnectionConfiguration.isSendPresence() final Presence presenceSend = this.presenceSend; if (connection.isAuthenticated() && presenceSend != null) { - Presence presence = presenceSend.asBuilder(connection).build(); try { - connection.sendStanza(presence); + connection.sendStanza(presenceSend.cloneWithNewId()); } catch (InterruptedException | NotConnectedException e) { LOGGER.log(Level.WARNING, "Could could not update presence with caps info", e); 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 b4d3f7554..7de48f1be 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 @@ -30,6 +30,7 @@ import org.jivesoftware.smack.packet.IqData; import org.jivesoftware.smack.util.EqualsUtil; import org.jivesoftware.smack.util.HashCode; import org.jivesoftware.smack.util.StringUtils; +import org.jivesoftware.smack.util.TypedCloneable; import org.jivesoftware.smack.util.XmlStringBuilder; import org.jxmpp.util.XmppStringUtils; @@ -43,7 +44,7 @@ import org.jxmpp.util.XmppStringUtils; * * @author Gaston Dombiak */ -public class DiscoverInfo extends IQ implements DiscoverInfoView { +public class DiscoverInfo extends IQ implements DiscoverInfoView, TypedCloneable { public static final String ELEMENT = QUERY_ELEMENT; public static final String NAMESPACE = "http://jabber.org/protocol/disco#info"; @@ -302,13 +303,7 @@ public class DiscoverInfo extends IQ implements DiscoverInfoView { return new DiscoverInfoBuilder(this, stanzaId); } - /** - * Deprecated, do not use. - * - * @deprecated use {@link #asBuilder(String)} instead. - */ - // TODO: Remove in Smack 4.5. - @Deprecated + // TODO: Deprecate in favor of asBuilder(). @Override public DiscoverInfo clone() { return new DiscoverInfo(this); @@ -521,7 +516,7 @@ public class DiscoverInfo extends IQ implements DiscoverInfoView { * as well as specific feature types of interest, if any (e.g., for the purpose of feature * negotiation). */ - public static final class Feature { + public static final class Feature implements TypedCloneable { private final String variable; @@ -571,6 +566,11 @@ public class DiscoverInfo extends IQ implements DiscoverInfoView { return variable.hashCode(); } + @Override + public Feature clone() { + return new Feature(this); + } + @Override public String toString() { return toXML().toString(); diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MucEnterConfiguration.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MucEnterConfiguration.java index 8a6b4b2fd..de2b6606e 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MucEnterConfiguration.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MucEnterConfiguration.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015-2020 Florian Schmaus + * Copyright 2015-2016 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -60,17 +60,15 @@ public final class MucEnterConfiguration { since = builder.since; timeout = builder.timeout; - final PresenceBuilder joinPresenceBuilder; if (builder.joinPresence == null) { - joinPresenceBuilder = builder.joinPresenceBuilder.ofType(Presence.Type.available); + joinPresence = builder.joinPresenceBuilder.ofType(Presence.Type.available).build(); } else { - joinPresenceBuilder = builder.joinPresence.asBuilder(); + joinPresence = builder.joinPresence.clone(); } // Indicate the the client supports MUC - joinPresenceBuilder.addExtension(new MUCInitialPresence(password, maxChars, maxStanzas, seconds, + joinPresence.addExtension(new MUCInitialPresence(password, maxChars, maxStanzas, seconds, since)); - joinPresence = joinPresenceBuilder.build(); } Presence getJoinPresence(MultiUserChat multiUserChat) { @@ -94,8 +92,6 @@ public final class MucEnterConfiguration { private long timeout; private final PresenceBuilder joinPresenceBuilder; - - // TODO: Remove in Smack 4.5. private Presence joinPresence; Builder(Resourcepart nickname, XMPPConnection connection) { diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChat.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChat.java index f5ee3ec53..40c3a85a2 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChat.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChat.java @@ -88,6 +88,7 @@ import org.jxmpp.jid.EntityJid; import org.jxmpp.jid.Jid; import org.jxmpp.jid.impl.JidCreate; import org.jxmpp.jid.parts.Resourcepart; +import org.jxmpp.util.cache.ExpirationCache; /** * A MultiUserChat room (XEP-45), created with {@link MultiUserChatManager#getMultiUserChat(EntityBareJid)}. @@ -111,6 +112,9 @@ import org.jxmpp.jid.parts.Resourcepart; public class MultiUserChat { private static final Logger LOGGER = Logger.getLogger(MultiUserChat.class.getName()); + private static final ExpirationCache KNOWN_MUC_SERVICES = new ExpirationCache<>( + 100, 1000 * 60 * 60 * 24); + private final XMPPConnection connection; private final EntityBareJid room; private final MultiUserChatManager multiUserChatManager; @@ -336,8 +340,12 @@ public class MultiUserChat { private Presence enter(MucEnterConfiguration conf) throws NotConnectedException, NoResponseException, XMPPErrorException, InterruptedException, NotAMucServiceException { final DomainBareJid mucService = room.asDomainBareJid(); - if (!multiUserChatManager.providesMucService(mucService)) { - throw new NotAMucServiceException(this); + if (!KNOWN_MUC_SERVICES.containsKey(mucService)) { + if (multiUserChatManager.providesMucService(mucService)) { + KNOWN_MUC_SERVICES.put(mucService, null); + } else { + throw new NotAMucServiceException(this); + } } // We enter a room by sending a presence packet where the "to" // field is in the form "roomName@service/nickname" 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 7d92e9817..e2654de89 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 @@ -64,7 +64,6 @@ import org.jxmpp.jid.EntityFullJid; import org.jxmpp.jid.EntityJid; import org.jxmpp.jid.Jid; import org.jxmpp.jid.parts.Resourcepart; -import org.jxmpp.util.cache.ExpirationCache; /** * A manager for Multi-User Chat rooms. @@ -137,9 +136,6 @@ public final class MultiUserChatManager extends Manager { private static final StanzaFilter INVITATION_FILTER = new AndFilter(StanzaTypeFilter.MESSAGE, new StanzaExtensionFilter(new MUCUser()), new NotFilter(MessageTypeFilter.ERROR)); - private static final ExpirationCache KNOWN_MUC_SERVICES = new ExpirationCache<>( - 100, 1000 * 60 * 60 * 24); - private final Set invitationsListeners = new CopyOnWriteArraySet(); /** @@ -396,16 +392,8 @@ public final class MultiUserChatManager extends Manager { */ public boolean providesMucService(DomainBareJid domainBareJid) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { - boolean contains = KNOWN_MUC_SERVICES.containsKey(domainBareJid); - if (!contains) { - if (serviceDiscoveryManager.supportsFeature(domainBareJid, - MUCInitialPresence.NAMESPACE)) { - KNOWN_MUC_SERVICES.put(domainBareJid, null); - return true; - } - } - - return contains; + return serviceDiscoveryManager.supportsFeature(domainBareJid, + MUCInitialPresence.NAMESPACE); } /** diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/AffiliationsExtension.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/AffiliationsExtension.java index c18a68243..2e30dd04c 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/AffiliationsExtension.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/AffiliationsExtension.java @@ -32,6 +32,7 @@ import org.jivesoftware.smackx.pubsub.Affiliation.AffiliationNamespace; */ public class AffiliationsExtension extends NodeExtension { protected List items = Collections.emptyList(); + private final String node; public AffiliationsExtension() { this(null); @@ -50,8 +51,9 @@ public class AffiliationsExtension extends NodeExtension { } public AffiliationsExtension(AffiliationNamespace affiliationsNamespace, List subList, String node) { - super(affiliationsNamespace.type, node); + super(affiliationsNamespace.type); items = subList; + this.node = node; } public List getAffiliations() { @@ -59,14 +61,19 @@ public class AffiliationsExtension extends NodeExtension { } @Override - protected void addXml(XmlStringBuilder xml) { + public CharSequence toXML(org.jivesoftware.smack.packet.XmlEnvironment enclosingNamespace) { if ((items == null) || (items.size() == 0)) { - xml.closeEmptyElement(); - return; + return super.toXML(enclosingNamespace); + } + else { + // Can't use XmlStringBuilder(this), because we don't want the namespace to be included + XmlStringBuilder xml = new XmlStringBuilder(); + xml.halfOpenElement(getElementName()); + xml.optAttribute("node", node); + xml.rightAngleBracket(); + xml.append(items); + xml.closeElement(this); + return xml; } - - xml.rightAngleBracket(); - xml.append(items); - xml.closeElement(this); } } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/FormNode.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/FormNode.java index 4d1a46cdd..6c3c69171 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/FormNode.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/FormNode.java @@ -16,8 +16,6 @@ */ package org.jivesoftware.smackx.pubsub; -import org.jivesoftware.smack.util.XmlStringBuilder; - import org.jivesoftware.smackx.xdata.packet.DataForm; /** @@ -71,14 +69,26 @@ public class FormNode extends NodeExtension { } @Override - protected void addXml(XmlStringBuilder xml) { + public CharSequence toXML(org.jivesoftware.smack.packet.XmlEnvironment enclosingNamespace) { if (configForm == null) { - xml.closeEmptyElement(); - return; + return super.toXML(enclosingNamespace); } + else { + StringBuilder builder = new StringBuilder("<"); + builder.append(getElementName()); - xml.append(configForm); - xml.closeElement(this); + if (getNode() != null) { + builder.append(" node='"); + builder.append(getNode()); + builder.append("'>"); + } + else + builder.append('>'); + builder.append(configForm.toXML()); + builder.append("'); + return builder.toString(); + } } } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/GetItemsRequest.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/GetItemsRequest.java index f929dd355..d55637a7c 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/GetItemsRequest.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/GetItemsRequest.java @@ -54,9 +54,13 @@ public class GetItemsRequest extends NodeExtension { } @Override - protected void addXml(XmlStringBuilder xml) { + public XmlStringBuilder toXML(org.jivesoftware.smack.packet.XmlEnvironment enclosingNamespace) { + XmlStringBuilder xml = new XmlStringBuilder(); + xml.halfOpenElement(getElementName()); + xml.attribute("node", getNode()); xml.optAttribute("subid", getSubscriptionId()); xml.optIntAttribute("max_items", getMaxItems()); xml.closeEmptyElement(); + return xml; } } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/Item.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/Item.java index 0cb4db2e3..727de6c48 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/Item.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/Item.java @@ -150,9 +150,21 @@ public class Item extends NodeExtension { } @Override - protected void addXml(XmlStringBuilder xml) { - xml.optAttribute("id", getId()); + public XmlStringBuilder toXML(org.jivesoftware.smack.packet.XmlEnvironment enclosingNamespace) { + XmlStringBuilder xml = getCommonXml(); + xml.closeEmptyElement(); + + return xml; + } + + protected final XmlStringBuilder getCommonXml() { + XmlStringBuilder xml = new XmlStringBuilder(this); + + xml.optAttribute("id", getId()); + xml.optAttribute("node", getNode()); + + return xml; } @Override diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/ItemsExtension.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/ItemsExtension.java index 02ae67901..000138897 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/ItemsExtension.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/ItemsExtension.java @@ -20,7 +20,6 @@ import java.util.List; import org.jivesoftware.smack.packet.ExtensionElement; import org.jivesoftware.smack.packet.NamedElement; -import org.jivesoftware.smack.util.XmlStringBuilder; /** * This class is used for multiple purposes. @@ -151,21 +150,35 @@ public class ItemsExtension extends NodeExtension implements EmbeddedPacketExten } @Override - protected void addXml(XmlStringBuilder xml) { + public CharSequence toXML(org.jivesoftware.smack.packet.XmlEnvironment enclosingNamespace) { if ((items == null) || (items.size() == 0)) { - xml.closeEmptyElement(); - return; + return super.toXML(enclosingNamespace); } + else { + StringBuilder builder = new StringBuilder("<"); + builder.append(getElementName()); + builder.append(" node='"); + builder.append(getNode()); - if (notify != null) { - xml.attribute(type.getElementAttribute(), notify); - xml.rightAngleBracket(); - } else { - xml.rightAngleBracket(); - xml.append(items); + if (notify != null) { + builder.append("' "); + builder.append(type.getElementAttribute()); + builder.append("='"); + builder.append(notify.equals(Boolean.TRUE) ? 1 : 0); + builder.append("'>"); + } + else { + builder.append("'>"); + for (NamedElement item : items) { + builder.append(item.toXML()); + } + } + + builder.append("'); + return builder.toString(); } - - xml.closeElement(this); } @Override diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/NodeExtension.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/NodeExtension.java index d89e438ca..cc8fac480 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/NodeExtension.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/NodeExtension.java @@ -17,8 +17,6 @@ package org.jivesoftware.smackx.pubsub; import org.jivesoftware.smack.packet.ExtensionElement; -import org.jivesoftware.smack.packet.XmlEnvironment; -import org.jivesoftware.smack.util.XmlStringBuilder; import org.jivesoftware.smackx.pubsub.packet.PubSubNamespace; @@ -80,17 +78,8 @@ public class NodeExtension implements ExtensionElement { } @Override - public final XmlStringBuilder toXML(XmlEnvironment enclosingNamespace) { - XmlStringBuilder xml = new XmlStringBuilder(this, enclosingNamespace); - xml.optAttribute("node", node); - - addXml(xml); - - return xml; - } - - protected void addXml(XmlStringBuilder xml) { - xml.closeEmptyElement(); + public CharSequence toXML(org.jivesoftware.smack.packet.XmlEnvironment enclosingNamespace) { + return '<' + getElementName() + (node == null ? "" : " node='" + node + '\'') + "/>"; } @Override diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/OptionsExtension.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/OptionsExtension.java index 7249f9eea..c9bbf6ab2 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/OptionsExtension.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/OptionsExtension.java @@ -50,15 +50,13 @@ public class OptionsExtension extends NodeExtension { } @Override - protected void addXml(XmlStringBuilder xml) { - xml.rightAngleBracket(); - + public XmlStringBuilder toXML(org.jivesoftware.smack.packet.XmlEnvironment enclosingNamespace) { + XmlStringBuilder xml = new XmlStringBuilder(); xml.halfOpenElement(getElementName()); xml.attribute("jid", jid); xml.optAttribute("node", getNode()); xml.optAttribute("subid", id); - xml.closeEmptyElement(); - xml.closeElement(this); + return xml; } } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/PayloadItem.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/PayloadItem.java index 6cb4210d8..f22a8e876 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/PayloadItem.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/PayloadItem.java @@ -132,11 +132,14 @@ public class PayloadItem extends Item { } @Override - protected void addXml(XmlStringBuilder xml) { - xml.optAttribute("id", getId()); + public XmlStringBuilder toXML(org.jivesoftware.smack.packet.XmlEnvironment enclosingNamespace) { + XmlStringBuilder xml = getCommonXml(); + xml.rightAngleBracket(); - xml.append(payload); + xml.append(payload.toXML()); xml.closeElement(this); + + return xml; } @Override diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/PublishItem.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/PublishItem.java index 7bdead52f..ec38e7aa5 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/PublishItem.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/PublishItem.java @@ -19,8 +19,6 @@ package org.jivesoftware.smackx.pubsub; import java.util.ArrayList; import java.util.Collection; -import org.jivesoftware.smack.util.XmlStringBuilder; - /** * Represents a request to publish an item(s) to a specific node. * @@ -53,9 +51,18 @@ public class PublishItem extends NodeExtension { } @Override - protected void addXml(XmlStringBuilder xml) { - xml.rightAngleBracket(); - xml.append(items); - xml.closeElement(this); + public String toXML(org.jivesoftware.smack.packet.XmlEnvironment enclosingNamespace) { + StringBuilder builder = new StringBuilder("<"); + builder.append(getElementName()); + builder.append(" node='"); + builder.append(getNode()); + builder.append("'>"); + + for (Item item : items) { + builder.append(item.toXML()); + } + builder.append(""); + + return builder.toString(); } } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/SubscribeExtension.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/SubscribeExtension.java index 64ef67d62..9c4a55b10 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/SubscribeExtension.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/SubscribeExtension.java @@ -16,6 +16,7 @@ */ package org.jivesoftware.smackx.pubsub; +import org.jivesoftware.smack.packet.XmlEnvironment; import org.jivesoftware.smack.util.XmlStringBuilder; import org.jxmpp.jid.Jid; @@ -43,8 +44,11 @@ public class SubscribeExtension extends NodeExtension { } @Override - protected void addXml(XmlStringBuilder xml) { + public XmlStringBuilder toXML(XmlEnvironment xmlEnvironment) { + XmlStringBuilder xml = new XmlStringBuilder(this, xmlEnvironment); + xml.optAttribute("node", getNode()); xml.attribute("jid", getJid()); xml.closeEmptyElement(); + return xml; } } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/Subscription.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/Subscription.java index c9b59896d..3b81ca016 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/Subscription.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/Subscription.java @@ -138,11 +138,16 @@ public class Subscription extends NodeExtension { } @Override - protected void addXml(XmlStringBuilder xml) { - xml.attribute("jid", jid); - xml.optAttribute("subid", id); - xml.optAttribute("subscription", state); - xml.closeEmptyElement(); + public XmlStringBuilder toXML(org.jivesoftware.smack.packet.XmlEnvironment enclosingNamespace) { + XmlStringBuilder builder = new XmlStringBuilder(this); + builder.attribute("jid", jid); + + builder.optAttribute("node", getNode()); + builder.optAttribute("subid", id); + builder.optAttribute("subscription", state.toString()); + + builder.closeEmptyElement(); + return builder; } } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/SubscriptionsExtension.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/SubscriptionsExtension.java index b9f3ee7df..8abde5836 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/SubscriptionsExtension.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/SubscriptionsExtension.java @@ -19,8 +19,6 @@ package org.jivesoftware.smackx.pubsub; import java.util.Collections; import java.util.List; -import org.jivesoftware.smack.util.XmlStringBuilder; - /** * Represents the element holding the list of subscription elements. * @@ -93,13 +91,29 @@ public class SubscriptionsExtension extends NodeExtension { } @Override - protected void addXml(XmlStringBuilder xml) { + public CharSequence toXML(org.jivesoftware.smack.packet.XmlEnvironment enclosingNamespace) { if ((items == null) || (items.size() == 0)) { - xml.closeEmptyElement(); - return; + return super.toXML(enclosingNamespace); + } + else { + StringBuilder builder = new StringBuilder("<"); + builder.append(getElementName()); + + if (getNode() != null) { + builder.append(" node='"); + builder.append(getNode()); + builder.append('\''); + } + builder.append('>'); + + for (Subscription item : items) { + builder.append(item.toXML()); + } + + builder.append("'); + return builder.toString(); } - xml.rightAngleBracket(); - xml.append(items); - xml.closeElement(this); } } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/UnsubscribeExtension.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/UnsubscribeExtension.java index 3c801ff21..107271786 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/UnsubscribeExtension.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/UnsubscribeExtension.java @@ -51,9 +51,13 @@ public class UnsubscribeExtension extends NodeExtension { } @Override - protected void addXml(XmlStringBuilder xml) { + public XmlStringBuilder toXML(org.jivesoftware.smack.packet.XmlEnvironment enclosingNamespace) { + XmlStringBuilder xml = new XmlStringBuilder(); + xml.halfOpenElement(getElementName()); xml.attribute("jid", jid); + xml.optAttribute("node", getNode()); xml.optAttribute("subid", id); xml.closeEmptyElement(); + return xml; } } diff --git a/smack-extensions/src/test/java/org/jivesoftware/smackx/attention/AttentionElementTest.java b/smack-extensions/src/test/java/org/jivesoftware/smackx/attention/AttentionElementTest.java index 755ca3ba0..dd01b7331 100644 --- a/smack-extensions/src/test/java/org/jivesoftware/smackx/attention/AttentionElementTest.java +++ b/smack-extensions/src/test/java/org/jivesoftware/smackx/attention/AttentionElementTest.java @@ -16,7 +16,7 @@ */ package org.jivesoftware.smackx.attention; -import static org.jivesoftware.smack.test.util.XmlAssertUtil.assertXmlSimilar; +import static org.jivesoftware.smack.test.util.XmlUnitUtils.assertXmlSimilar; import org.jivesoftware.smackx.attention.packet.AttentionExtension; diff --git a/smack-extensions/src/test/java/org/jivesoftware/smackx/bytestreams/ibb/packet/CloseTest.java b/smack-extensions/src/test/java/org/jivesoftware/smackx/bytestreams/ibb/packet/CloseTest.java index 07dd9920e..8fbb488fc 100644 --- a/smack-extensions/src/test/java/org/jivesoftware/smackx/bytestreams/ibb/packet/CloseTest.java +++ b/smack-extensions/src/test/java/org/jivesoftware/smackx/bytestreams/ibb/packet/CloseTest.java @@ -16,7 +16,7 @@ */ package org.jivesoftware.smackx.bytestreams.ibb.packet; -import static org.jivesoftware.smack.test.util.XmlAssertUtil.assertXmlSimilar; +import static org.jivesoftware.smack.test.util.XmlUnitUtils.assertXmlSimilar; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; diff --git a/smack-extensions/src/test/java/org/jivesoftware/smackx/bytestreams/ibb/packet/DataPacketExtensionTest.java b/smack-extensions/src/test/java/org/jivesoftware/smackx/bytestreams/ibb/packet/DataPacketExtensionTest.java index beab45d02..3042ae07a 100644 --- a/smack-extensions/src/test/java/org/jivesoftware/smackx/bytestreams/ibb/packet/DataPacketExtensionTest.java +++ b/smack-extensions/src/test/java/org/jivesoftware/smackx/bytestreams/ibb/packet/DataPacketExtensionTest.java @@ -16,7 +16,7 @@ */ package org.jivesoftware.smackx.bytestreams.ibb.packet; -import static org.jivesoftware.smack.test.util.XmlAssertUtil.assertXmlSimilar; +import static org.jivesoftware.smack.test.util.XmlUnitUtils.assertXmlSimilar; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; diff --git a/smack-extensions/src/test/java/org/jivesoftware/smackx/bytestreams/ibb/packet/DataTest.java b/smack-extensions/src/test/java/org/jivesoftware/smackx/bytestreams/ibb/packet/DataTest.java index 2e0a0cacf..aa24c043d 100644 --- a/smack-extensions/src/test/java/org/jivesoftware/smackx/bytestreams/ibb/packet/DataTest.java +++ b/smack-extensions/src/test/java/org/jivesoftware/smackx/bytestreams/ibb/packet/DataTest.java @@ -16,7 +16,7 @@ */ package org.jivesoftware.smackx.bytestreams.ibb.packet; -import static org.jivesoftware.smack.test.util.XmlAssertUtil.assertXmlSimilar; +import static org.jivesoftware.smack.test.util.XmlUnitUtils.assertXmlSimilar; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.mock; diff --git a/smack-extensions/src/test/java/org/jivesoftware/smackx/bytestreams/ibb/packet/OpenTest.java b/smack-extensions/src/test/java/org/jivesoftware/smackx/bytestreams/ibb/packet/OpenTest.java index d28cd617f..8206efbf2 100644 --- a/smack-extensions/src/test/java/org/jivesoftware/smackx/bytestreams/ibb/packet/OpenTest.java +++ b/smack-extensions/src/test/java/org/jivesoftware/smackx/bytestreams/ibb/packet/OpenTest.java @@ -16,7 +16,7 @@ */ package org.jivesoftware.smackx.bytestreams.ibb.packet; -import static org.jivesoftware.smack.test.util.XmlAssertUtil.assertXmlSimilar; +import static org.jivesoftware.smack.test.util.XmlUnitUtils.assertXmlSimilar; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; diff --git a/smack-extensions/src/test/java/org/jivesoftware/smackx/last_interaction/IdleTest.java b/smack-extensions/src/test/java/org/jivesoftware/smackx/last_interaction/IdleTest.java index bf8c6ae27..3a6da6b49 100644 --- a/smack-extensions/src/test/java/org/jivesoftware/smackx/last_interaction/IdleTest.java +++ b/smack-extensions/src/test/java/org/jivesoftware/smackx/last_interaction/IdleTest.java @@ -16,7 +16,7 @@ */ package org.jivesoftware.smackx.last_interaction; -import static org.jivesoftware.smack.test.util.XmlAssertUtil.assertXmlSimilar; +import static org.jivesoftware.smack.test.util.XmlUnitUtils.assertXmlSimilar; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; diff --git a/smack-extensions/src/test/java/org/jivesoftware/smackx/mood/MoodConcretisationTest.java b/smack-extensions/src/test/java/org/jivesoftware/smackx/mood/MoodConcretisationTest.java index f05bec4a2..4ad647e29 100644 --- a/smack-extensions/src/test/java/org/jivesoftware/smackx/mood/MoodConcretisationTest.java +++ b/smack-extensions/src/test/java/org/jivesoftware/smackx/mood/MoodConcretisationTest.java @@ -16,7 +16,7 @@ */ package org.jivesoftware.smackx.mood; -import static org.jivesoftware.smack.test.util.XmlAssertUtil.assertXmlSimilar; +import static org.jivesoftware.smack.test.util.XmlUnitUtils.assertXmlSimilar; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; diff --git a/smack-extensions/src/test/java/org/jivesoftware/smackx/mood/MoodElementTest.java b/smack-extensions/src/test/java/org/jivesoftware/smackx/mood/MoodElementTest.java index c1cb72fd5..31d937be8 100644 --- a/smack-extensions/src/test/java/org/jivesoftware/smackx/mood/MoodElementTest.java +++ b/smack-extensions/src/test/java/org/jivesoftware/smackx/mood/MoodElementTest.java @@ -16,7 +16,7 @@ */ package org.jivesoftware.smackx.mood; -import static org.jivesoftware.smack.test.util.XmlAssertUtil.assertXmlSimilar; +import static org.jivesoftware.smack.test.util.XmlUnitUtils.assertXmlSimilar; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; diff --git a/smack-extensions/src/test/java/org/jivesoftware/smackx/nick/NickTest.java b/smack-extensions/src/test/java/org/jivesoftware/smackx/nick/NickTest.java index f4379e034..61c7b70eb 100644 --- a/smack-extensions/src/test/java/org/jivesoftware/smackx/nick/NickTest.java +++ b/smack-extensions/src/test/java/org/jivesoftware/smackx/nick/NickTest.java @@ -16,7 +16,7 @@ */ package org.jivesoftware.smackx.nick; -import static org.jivesoftware.smack.test.util.XmlAssertUtil.assertXmlSimilar; +import static org.jivesoftware.smack.test.util.XmlUnitUtils.assertXmlSimilar; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; diff --git a/smack-extensions/src/test/java/org/jivesoftware/smackx/pubsub/AffiliationsExtensionTest.java b/smack-extensions/src/test/java/org/jivesoftware/smackx/pubsub/AffiliationsExtensionTest.java index 8c1706091..5a03d5ed3 100644 --- a/smack-extensions/src/test/java/org/jivesoftware/smackx/pubsub/AffiliationsExtensionTest.java +++ b/smack-extensions/src/test/java/org/jivesoftware/smackx/pubsub/AffiliationsExtensionTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2017-2020 Florian Schmaus + * Copyright 2017 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,14 +16,13 @@ */ package org.jivesoftware.smackx.pubsub; -import static org.jivesoftware.smack.test.util.XmlAssertUtil.assertXmlSimilar; +import static org.jivesoftware.smack.test.util.XmlUnitUtils.assertXmlSimilar; import java.io.IOException; import java.util.ArrayList; import java.util.List; import org.jivesoftware.smackx.pubsub.Affiliation.Type; -import org.jivesoftware.smackx.pubsub.packet.PubSub; import org.junit.jupiter.api.Test; import org.jxmpp.jid.BareJid; @@ -41,10 +40,10 @@ public class AffiliationsExtensionTest { AffiliationsExtension affiliationsExtension = new AffiliationsExtension(affiliationsList, "testNode"); - CharSequence xml = affiliationsExtension.toXML(PubSub.NAMESPACE); + CharSequence xml = affiliationsExtension.toXML(); assertXmlSimilar("", - xml); + xml.toString()); } } diff --git a/smack-extensions/src/test/java/org/jivesoftware/smackx/pubsub/ItemValidationTest.java b/smack-extensions/src/test/java/org/jivesoftware/smackx/pubsub/ItemValidationTest.java index 8ed23eed8..9dafb5334 100644 --- a/smack-extensions/src/test/java/org/jivesoftware/smackx/pubsub/ItemValidationTest.java +++ b/smack-extensions/src/test/java/org/jivesoftware/smackx/pubsub/ItemValidationTest.java @@ -16,7 +16,7 @@ */ package org.jivesoftware.smackx.pubsub; -import static org.jivesoftware.smack.test.util.XmlAssertUtil.assertXmlSimilar; +import static org.jivesoftware.smack.test.util.XmlUnitUtils.assertXmlSimilar; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; diff --git a/smack-extensions/src/test/java/org/jivesoftware/smackx/softwareinfo/SoftwareInfoFormTest.java b/smack-extensions/src/test/java/org/jivesoftware/smackx/softwareinfo/SoftwareInfoFormTest.java index ffeed72aa..48613cd02 100644 --- a/smack-extensions/src/test/java/org/jivesoftware/smackx/softwareinfo/SoftwareInfoFormTest.java +++ b/smack-extensions/src/test/java/org/jivesoftware/smackx/softwareinfo/SoftwareInfoFormTest.java @@ -16,7 +16,7 @@ */ package org.jivesoftware.smackx.softwareinfo; -import static org.jivesoftware.smack.test.util.XmlAssertUtil.assertXmlSimilar; +import static org.jivesoftware.smack.test.util.XmlUnitUtils.assertXmlSimilar; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; diff --git a/smack-extensions/src/test/java/org/jivesoftware/smackx/usertune/UserTuneElementTest.java b/smack-extensions/src/test/java/org/jivesoftware/smackx/usertune/UserTuneElementTest.java index 59c912eaf..b9bdb1189 100644 --- a/smack-extensions/src/test/java/org/jivesoftware/smackx/usertune/UserTuneElementTest.java +++ b/smack-extensions/src/test/java/org/jivesoftware/smackx/usertune/UserTuneElementTest.java @@ -16,7 +16,7 @@ */ package org.jivesoftware.smackx.usertune; -import static org.jivesoftware.smack.test.util.XmlAssertUtil.assertXmlSimilar; +import static org.jivesoftware.smack.test.util.XmlUnitUtils.assertXmlSimilar; import java.io.IOException; import java.net.URI; diff --git a/smack-extensions/src/test/java/org/jivesoftware/smackx/xdata/FormFieldTest.java b/smack-extensions/src/test/java/org/jivesoftware/smackx/xdata/FormFieldTest.java index 67cd5dd89..214c67e29 100644 --- a/smack-extensions/src/test/java/org/jivesoftware/smackx/xdata/FormFieldTest.java +++ b/smack-extensions/src/test/java/org/jivesoftware/smackx/xdata/FormFieldTest.java @@ -16,7 +16,7 @@ */ package org.jivesoftware.smackx.xdata; -import static org.jivesoftware.smack.test.util.XmlAssertUtil.assertXmlSimilar; +import static org.jivesoftware.smack.test.util.XmlUnitUtils.assertXmlSimilar; import org.junit.jupiter.api.Test; import org.jxmpp.jid.JidTestUtil; 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 02ee0d7e1..dd05ac97b 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 @@ -1047,7 +1047,7 @@ public final class Roster extends Manager { } if (presence == null) { if (unavailable != null) { - return unavailable; + return unavailable.clone(); } else { presence = synthesizeUnvailablePresence(jid); @@ -1055,7 +1055,7 @@ public final class Roster extends Manager { } } else { - return presence; + return presence.clone(); } } } @@ -1084,7 +1084,7 @@ public final class Roster extends Manager { return presence; } else { - return presence; + return presence.clone(); } } } @@ -1107,7 +1107,7 @@ public final class Roster extends Manager { } else { res = new ArrayList<>(userPresences.values().size()); for (Presence presence : userPresences.values()) { - res.add(presence); + res.add(presence.clone()); } } return res; @@ -1156,7 +1156,7 @@ public final class Roster extends Manager { Presence unavailable = null; for (Presence presence : userPresences.values()) { if (presence.isAvailable()) { - answer.add(presence); + answer.add(presence.clone()); } else { unavailable = presence; @@ -1166,7 +1166,7 @@ public final class Roster extends Manager { res = answer; } else if (unavailable != null) { - res = Arrays.asList(unavailable); + res = Arrays.asList(unavailable.clone()); } else { Presence presence = synthesizeUnvailablePresence(jid); diff --git a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/SmackIntegrationTestFramework.java b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/SmackIntegrationTestFramework.java index e8d7758f8..b89402df8 100644 --- a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/SmackIntegrationTestFramework.java +++ b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/SmackIntegrationTestFramework.java @@ -454,12 +454,10 @@ public class SmackIntegrationTestFramework { } sb.append('\n'); } - sb.append("Available tests: ").append(numberOfAvailableTests); - if (!testRunResult.disabledTestClasses.isEmpty() || !testRunResult.disabledTests.isEmpty()) { - sb.append(" (Disabled ").append(testRunResult.disabledTestClasses.size()).append(" classes") - .append(" and ").append(testRunResult.disabledTests.size()).append(" tests"); - } - sb.append('\n'); + sb.append("Available tests: ").append(numberOfAvailableTests) + .append("(#-classes: ").append(testRunResult.disabledTestClasses.size()) + .append(", #-tests: ").append(testRunResult.disabledTests.size()) + .append(")\n"); LOGGER.info(sb.toString()); for (PreparedTest test : tests) { @@ -865,8 +863,6 @@ public class SmackIntegrationTestFramework { } testMethod.invoke(test, connectionsArray); } - - connectionManager.recycle(connections); } @Override diff --git a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/XmppConnectionManager.java b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/XmppConnectionManager.java index 204562fd4..207ab1c02 100644 --- a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/XmppConnectionManager.java +++ b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/XmppConnectionManager.java @@ -480,8 +480,6 @@ public class XmppConnectionManager { synchronized (connectionPool) { connectionPool.put(connectionClass, connection); } - } else { - connection.disconnect(); } // Note that we do not delete the account of the unauthenticated connection here, as it is done at the end of // the test run together with all other dynamically created accounts. diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smack/LoginIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smack/LoginIntegrationTest.java index 7534c4449..47c299671 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smack/LoginIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smack/LoginIntegrationTest.java @@ -58,15 +58,11 @@ public class LoginIntegrationTest extends AbstractSmackLowLevelIntegrationTest { AbstractXMPPConnection connection = getUnconnectedConnection(); connection.connect(); - try { - SASLErrorException saslErrorException = assertThrows(SASLErrorException.class, - () -> connection.login(nonExistentUserString, invalidPassword)); + SASLErrorException saslErrorException = assertThrows(SASLErrorException.class, + () -> connection.login(nonExistentUserString, invalidPassword)); - SaslNonza.SASLFailure saslFailure = saslErrorException.getSASLFailure(); - assertEquals(SASLError.not_authorized, saslFailure.getSASLError()); - } finally { - connection.disconnect(); - } + SaslNonza.SASLFailure saslFailure = saslErrorException.getSASLFailure(); + assertEquals(SASLError.not_authorized, saslFailure.getSASLError()); } } diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smack/WaitForClosingStreamElementTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smack/WaitForClosingStreamElementTest.java index 2326866c7..023979d64 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smack/WaitForClosingStreamElementTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smack/WaitForClosingStreamElementTest.java @@ -38,7 +38,12 @@ public class WaitForClosingStreamElementTest extends AbstractSmackLowLevelIntegr Field closingStreamReceivedField = AbstractXMPPConnection.class.getDeclaredField("closingStreamReceived"); closingStreamReceivedField.setAccessible(true); - boolean closingStreamReceived = (boolean) closingStreamReceivedField.get(connection); - assertTrue(closingStreamReceived); + SynchronizationPoint closingStreamReceived = (SynchronizationPoint) closingStreamReceivedField.get(connection); + Exception failureException = closingStreamReceived.getFailureException(); + if (failureException != null) { + throw new AssertionError("Sync poing yielded failure exception", failureException); + } + boolean closingStreamReceivedSuccessful = closingStreamReceived.wasSuccessful(); + assertTrue(closingStreamReceivedSuccessful); } } 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 8943e9023..7ba0c1d1e 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 @@ -20,7 +20,6 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.List; -import java.util.concurrent.TimeoutException; import org.jivesoftware.smack.MessageListener; import org.jivesoftware.smack.SmackException.NoResponseException; @@ -40,7 +39,6 @@ import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; import org.igniterealtime.smack.inttest.TestNotPossibleException; import org.igniterealtime.smack.inttest.annotations.SmackIntegrationTest; import org.igniterealtime.smack.inttest.util.ResultSyncPoint; -import org.igniterealtime.smack.inttest.util.SimpleResultSyncPoint; import org.jxmpp.jid.DomainBareJid; import org.jxmpp.jid.EntityBareJid; import org.jxmpp.jid.impl.JidCreate; @@ -122,41 +120,4 @@ public class MultiUserChatIntegrationTest extends AbstractSmackIntegrationTest { mucAsSeenByOne.leave(); mucAsSeenByTwo.leave(); } - - @SmackIntegrationTest - public void mucDestroyTest() throws TimeoutException, Exception { - - EntityBareJid mucAddress = JidCreate.entityBareFrom(Localpart.from("smack-inttest-join-leave-" + randomString), - mucService.getDomain()); - - MultiUserChat muc = mucManagerOne.getMultiUserChat(mucAddress); - muc.join(Resourcepart.from("nick-one")); - - final SimpleResultSyncPoint mucDestroyed = new SimpleResultSyncPoint(); - - @SuppressWarnings("deprecation") - DefaultUserStatusListener userStatusListener = new DefaultUserStatusListener() { - @Override - public void roomDestroyed(MultiUserChat alternateMUC, String reason) { - mucDestroyed.signal(); - } - }; - - muc.addUserStatusListener(userStatusListener); - - assertTrue(mucManagerOne.getJoinedRooms().size() == 1); - assertTrue(muc.getOccupantsCount() == 1); - assertTrue(muc.getNickname() != null); - - try { - muc.destroy("Dummy reason", null); - mucDestroyed.waitForResult(timeout); - } finally { - muc.removeUserStatusListener(userStatusListener); - } - - assertTrue(mucManagerOne.getJoinedRooms().size() == 0); - assertTrue(muc.getOccupantsCount() == 0); - assertTrue(muc.getNickname() == null); - } } diff --git a/smack-openpgp/src/test/java/org/jivesoftware/smackx/ox/OpenPgpElementTest.java b/smack-openpgp/src/test/java/org/jivesoftware/smackx/ox/OpenPgpElementTest.java index 143580531..2d8d55419 100644 --- a/smack-openpgp/src/test/java/org/jivesoftware/smackx/ox/OpenPgpElementTest.java +++ b/smack-openpgp/src/test/java/org/jivesoftware/smackx/ox/OpenPgpElementTest.java @@ -18,7 +18,7 @@ package org.jivesoftware.smackx.ox; import static junit.framework.TestCase.assertEquals; import static junit.framework.TestCase.assertNotNull; -import static org.jivesoftware.smack.test.util.XmlAssertUtil.assertXmlSimilar; +import static org.jivesoftware.smack.test.util.XmlUnitUtils.assertXmlSimilar; import static org.junit.jupiter.api.Assertions.assertThrows; import java.io.IOException; diff --git a/smack-openpgp/src/test/java/org/jivesoftware/smackx/ox/PubkeyElementTest.java b/smack-openpgp/src/test/java/org/jivesoftware/smackx/ox/PubkeyElementTest.java index 46e053891..a67d4870d 100644 --- a/smack-openpgp/src/test/java/org/jivesoftware/smackx/ox/PubkeyElementTest.java +++ b/smack-openpgp/src/test/java/org/jivesoftware/smackx/ox/PubkeyElementTest.java @@ -17,7 +17,7 @@ package org.jivesoftware.smackx.ox; import static junit.framework.TestCase.assertEquals; -import static org.jivesoftware.smack.test.util.XmlAssertUtil.assertXmlSimilar; +import static org.jivesoftware.smack.test.util.XmlUnitUtils.assertXmlSimilar; import java.io.IOException; import java.text.ParseException; diff --git a/smack-openpgp/src/test/java/org/jivesoftware/smackx/ox/PublicKeysListElementTest.java b/smack-openpgp/src/test/java/org/jivesoftware/smackx/ox/PublicKeysListElementTest.java index f90015b9f..54f2f98cc 100644 --- a/smack-openpgp/src/test/java/org/jivesoftware/smackx/ox/PublicKeysListElementTest.java +++ b/smack-openpgp/src/test/java/org/jivesoftware/smackx/ox/PublicKeysListElementTest.java @@ -17,7 +17,7 @@ package org.jivesoftware.smackx.ox; import static junit.framework.TestCase.assertEquals; -import static org.jivesoftware.smack.test.util.XmlAssertUtil.assertXmlSimilar; +import static org.jivesoftware.smack.test.util.XmlUnitUtils.assertXmlSimilar; import java.util.Date; diff --git a/smack-openpgp/src/test/java/org/jivesoftware/smackx/ox/SecretkeyElementTest.java b/smack-openpgp/src/test/java/org/jivesoftware/smackx/ox/SecretkeyElementTest.java index b9745bb20..56902d75f 100644 --- a/smack-openpgp/src/test/java/org/jivesoftware/smackx/ox/SecretkeyElementTest.java +++ b/smack-openpgp/src/test/java/org/jivesoftware/smackx/ox/SecretkeyElementTest.java @@ -17,7 +17,7 @@ package org.jivesoftware.smackx.ox; import static junit.framework.TestCase.assertTrue; -import static org.jivesoftware.smack.test.util.XmlAssertUtil.assertXmlSimilar; +import static org.jivesoftware.smack.test.util.XmlUnitUtils.assertXmlSimilar; import java.nio.charset.Charset; import java.util.Arrays; diff --git a/smack-openpgp/src/test/java/org/jivesoftware/smackx/ox_im/OXInstantMessagingManagerTest.java b/smack-openpgp/src/test/java/org/jivesoftware/smackx/ox_im/OXInstantMessagingManagerTest.java index e7106820a..fbf33e6a3 100644 --- a/smack-openpgp/src/test/java/org/jivesoftware/smackx/ox_im/OXInstantMessagingManagerTest.java +++ b/smack-openpgp/src/test/java/org/jivesoftware/smackx/ox_im/OXInstantMessagingManagerTest.java @@ -56,6 +56,7 @@ import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.jupiter.api.Test; import org.jxmpp.jid.EntityBareJid; +import org.jxmpp.jid.JidTestUtil; import org.pgpainless.decryption_verification.OpenPgpMetadata; public class OXInstantMessagingManagerTest extends SmackTestSuite { @@ -70,10 +71,16 @@ public class OXInstantMessagingManagerTest extends SmackTestSuite { public void test() throws IOException, PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, NoSuchProviderException, SmackException, MissingUserIdOnKeyException, InterruptedException, XMPPException, XmlPullParserException { - DummyConnection aliceCon = new DummyConnection(); + DummyConnection aliceCon = new DummyConnection( + DummyConnection.DummyConnectionConfiguration.builder() + .setXmppDomain(JidTestUtil.EXAMPLE_ORG) + .setUsernameAndPassword("alice", "dummypass").build()); aliceCon.connect().login(); - DummyConnection bobCon = new DummyConnection(); + DummyConnection bobCon = new DummyConnection( + DummyConnection.DummyConnectionConfiguration.builder() + .setXmppDomain(JidTestUtil.EXAMPLE_ORG) + .setUsernameAndPassword("bob", "dummypass").build()); bobCon.connect().login(); FileBasedOpenPgpStore aliceStore = new FileBasedOpenPgpStore(new File(basePath, "alice")); diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/ConnectionAttemptState.java b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/ConnectionAttemptState.java index d8a3890e0..415af19f9 100644 --- a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/ConnectionAttemptState.java +++ b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/ConnectionAttemptState.java @@ -26,9 +26,10 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import org.jivesoftware.smack.SmackException.ConnectionException; import org.jivesoftware.smack.SmackException.EndpointConnectionException; +import org.jivesoftware.smack.SynchronizationPoint; import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal; -import org.jivesoftware.smack.fsm.StateTransitionResult; import org.jivesoftware.smack.tcp.XmppTcpTransportModule.EstablishingTcpConnectionState; import org.jivesoftware.smack.tcp.rce.Rfc6120TcpRemoteConnectionEndpoint; import org.jivesoftware.smack.util.Async; @@ -47,10 +48,7 @@ public final class ConnectionAttemptState { final SocketChannel socketChannel; final List> connectionExceptions; - - EndpointConnectionException connectionException; - boolean connected; - long deadline; + final SynchronizationPoint tcpConnectionEstablishedSyncPoint; final Iterator connectionEndpointIterator; /** The current connection endpoint we are trying */ @@ -67,32 +65,17 @@ public final class ConnectionAttemptState { socketChannel = SocketChannel.open(); socketChannel.configureBlocking(false); - List endpoints = discoveredEndpoints.result.discoveredRemoteConnectionEndpoints; - connectionEndpointIterator = endpoints.iterator(); + connectionEndpointIterator = discoveredEndpoints.result.discoveredRemoteConnectionEndpoints.iterator(); connectionEndpoint = connectionEndpointIterator.next(); - connectionExceptions = new ArrayList<>(endpoints.size()); + connectionExceptions = new ArrayList<>(discoveredEndpoints.result.discoveredRemoteConnectionEndpoints.size()); + + tcpConnectionEstablishedSyncPoint = new SynchronizationPoint<>(connectionInternal.connection, + "TCP connection establishment"); } - StateTransitionResult.Failure establishTcpConnection() throws InterruptedException { + void establishTcpConnection() { RemoteConnectionEndpoint.InetSocketAddressCoupling address = nextAddress(); establishTcpConnection(address); - - synchronized (this) { - while (!connected && connectionException == null) { - final long now = System.currentTimeMillis(); - if (now >= deadline) { - return new StateTransitionResult.FailureCausedByTimeout("Timeout waiting to establish connection"); - } - wait (deadline - now); - } - } - if (connected) { - assert connectionException == null; - // Success case: we have been able to establish a connection to one remote endpoint. - return null; - } - - return new StateTransitionResult.FailureCausedByException(connectionException); } private void establishTcpConnection( @@ -101,10 +84,8 @@ public final class ConnectionAttemptState { establishingTcpConnectionState, address); connectionInternal.invokeConnectionStateMachineListener(connectingToHostEvent); + final boolean connected; final InetSocketAddress inetSocketAddress = address.getInetSocketAddress(); - // TODO: Should use "connect timeout" instead of reply timeout. But first connect timeout needs to be moved from - // XMPPTCPConnectionConfiguration. into XMPPConnectionConfiguration. - deadline = System.currentTimeMillis() + connectionInternal.connection.getReplyTimeout(); try { connected = socketChannel.connect(inetSocketAddress); } catch (IOException e) { @@ -117,9 +98,7 @@ public final class ConnectionAttemptState { establishingTcpConnectionState, address, true); connectionInternal.invokeConnectionStateMachineListener(connectedToHostEvent); - synchronized (this) { - notifyAll(); - } + tcpConnectionEstablishedSyncPoint.reportSuccess(); return; } @@ -145,10 +124,9 @@ public final class ConnectionAttemptState { establishingTcpConnectionState, address, false); connectionInternal.invokeConnectionStateMachineListener(connectedToHostEvent); - connected = true; - synchronized (ConnectionAttemptState.this) { - notifyAll(); - } + // Do not set 'state' here, since this is processed by a reactor thread, which doesn't hold + // the objects lock. + tcpConnectionEstablishedSyncPoint.reportSuccess(); }); } catch (ClosedChannelException e) { onIOExceptionWhenEstablishingTcpConnection(e, address); @@ -159,14 +137,14 @@ public final class ConnectionAttemptState { RemoteConnectionEndpoint.InetSocketAddressCoupling failedAddress) { RemoteConnectionEndpoint.InetSocketAddressCoupling nextInetSocketAddress = nextAddress(); if (nextInetSocketAddress == null) { - connectionException = EndpointConnectionException.from( + EndpointConnectionException connectionException = EndpointConnectionException.from( discoveredEndpoints.result.lookupFailures, connectionExceptions); - synchronized (this) { - notifyAll(); - } + tcpConnectionEstablishedSyncPoint.reportFailure(connectionException); return; } + tcpConnectionEstablishedSyncPoint.resetTimeout(); + RemoteConnectionException rce = new RemoteConnectionException<>( failedAddress, exception); connectionExceptions.add(rce); 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 ded929681..ea34b9518 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 @@ -26,6 +26,11 @@ import java.io.Writer; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Socket; +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; import java.util.ArrayList; import java.util.Collection; @@ -39,6 +44,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; @@ -59,12 +65,13 @@ import org.jivesoftware.smack.SmackException.AlreadyConnectedException; import org.jivesoftware.smack.SmackException.AlreadyLoggedInException; import org.jivesoftware.smack.SmackException.ConnectionException; import org.jivesoftware.smack.SmackException.EndpointConnectionException; +import org.jivesoftware.smack.SmackException.NoResponseException; import org.jivesoftware.smack.SmackException.NotConnectedException; import org.jivesoftware.smack.SmackException.NotLoggedInException; -import org.jivesoftware.smack.SmackException.SecurityNotPossibleException; import org.jivesoftware.smack.SmackException.SecurityRequiredByServerException; import org.jivesoftware.smack.SmackFuture; import org.jivesoftware.smack.StanzaListener; +import org.jivesoftware.smack.SynchronizationPoint; import org.jivesoftware.smack.XMPPConnection; import org.jivesoftware.smack.XMPPException; import org.jivesoftware.smack.XMPPException.FailedNonzaException; @@ -144,6 +151,8 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { private SSLSocket secureSocket; + private final Semaphore readerWriterSemaphore = new Semaphore(2); + /** * Protected access level because of unit test purposes */ @@ -157,12 +166,14 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { /** * */ - private boolean streamFeaturesAfterAuthenticationReceived; + private final SynchronizationPoint maybeCompressFeaturesReceived = new SynchronizationPoint( + this, "stream compression feature"); /** * */ - private boolean compressSyncPoint; + private final SynchronizationPoint compressSyncPoint = new SynchronizationPoint<>( + this, "stream compression"); /** * The default bundle and defer callback, used for new connections. @@ -186,26 +197,15 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { /** * The stream ID of the stream that is currently resumable, ie. the stream we hold the state * for in {@link #clientHandledStanzasCount}, {@link #serverHandledStanzasCount} and - * {@link #unFailedNonzaExceptionacknowledgedStanzas}. + * {@link #unacknowledgedStanzas}. */ private String smSessionId; - /** - * Represents the state of stream management resumption. - *

- * Unlike other sync points, this sync point is marked volatile because it is also read by the reader thread. - *

- */ - private volatile SyncPointState smResumedSyncPoint; - private Failed smResumptionFailed; + private final SynchronizationPoint smResumedSyncPoint = new SynchronizationPoint<>( + this, "stream resumed element"); - /** - * Represents the state of stream magement. - *

- * This boolean is marked volatile as it is read by various threads, including the reader thread via {@link #isSmEnabled()}. - *

- */ - private volatile boolean smEnabledSyncPoint; + private final SynchronizationPoint smEnabledSyncPoint = new SynchronizationPoint<>( + this, "stream enabled element"); /** * The client's preferred maximum resumption time in seconds. @@ -376,26 +376,20 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { SmackException, IOException, InterruptedException { // Authenticate using SASL SSLSession sslSession = secureSocket != null ? secureSocket.getSession() : null; - - streamFeaturesAfterAuthenticationReceived = false; 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". - waitForConditionOrThrowConnectionException(() -> streamFeaturesAfterAuthenticationReceived, "compress features from server"); + 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(); - smResumedSyncPoint = SyncPointState.initial; - smResumptionFailed = null; if (isSmResumptionPossible()) { - smResumedSyncPoint = SyncPointState.request_sent; - sendNonza(new Resume(clientHandledStanzasCount, smSessionId)); - waitForCondition(() -> smResumedSyncPoint == SyncPointState.successful || smResumptionFailed != null, "resume previous stream"); - if (smResumedSyncPoint == SyncPointState.successful) { + smResumedSyncPoint.sendAndWaitForResponse(new Resume(clientHandledStanzasCount, smSessionId)); + if (smResumedSyncPoint.wasSuccessful()) { // We successfully resumed the stream, be done here afterSuccessfulLogin(true); return; @@ -403,8 +397,7 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { // SM resumption failed, what Smack does here is to report success of // lastFeaturesReceived in case of sm resumption was answered with 'failed' so that // normal resource binding can be tried. - assert smResumptionFailed != null; - LOGGER.fine("Stream resumption failed, continuing with normal stream establishment process: " + smResumptionFailed); + LOGGER.fine("Stream resumption failed, continuing with normal stream establishment process"); } List previouslyUnackedStanzas = new LinkedList(); @@ -425,14 +418,12 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { // unacknowledgedStanzas and become duplicated on reconnect. See SMACK-706. bindResourceAndEstablishSession(resource); - smEnabledSyncPoint = false; if (isSmAvailable() && useSm) { // Remove what is maybe left from previously stream managed sessions serverHandledStanzasCount = 0; - sendNonza(new Enable(useSmResumption, smClientMaxResumptionTime)); // XEP-198 3. Enabling Stream Management. If the server response to 'Enable' is 'Failed' // then this is a non recoverable error and we therefore throw an exception. - waitForConditionOrThrowConnectionException(() -> smEnabledSyncPoint, "enabling stream mangement"); + smEnabledSyncPoint.sendAndWaitForResponseOrThrow(new Enable(useSmResumption, smClientMaxResumptionTime)); synchronized (requestAckPredicates) { if (requestAckPredicates.isEmpty()) { // Assure that we have at lest one predicate set up that so that we request acks @@ -494,19 +485,14 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { } private void shutdown(boolean instant) { - // The writer thread may already been finished at this point, for example when the connection is in the - // disconnected-but-resumable state. There is no need to wait for the closing stream tag from the server in this - // case. - if (!packetWriter.done()) { - // First shutdown the writer, this will result in a closing stream element getting send to - // the server - LOGGER.finer("PacketWriter shutdown()"); - packetWriter.shutdown(instant); - LOGGER.finer("PacketWriter has been shut down"); + // First shutdown the writer, this will result in a closing stream element getting send to + // the server + LOGGER.finer("PacketWriter shutdown()"); + packetWriter.shutdown(instant); + LOGGER.finer("PacketWriter has been shut down"); - if (!instant) { - waitForClosingStreamTagFromServer(); - } + if (!instant) { + waitForClosingStreamTagFromServer(); } LOGGER.finer("PacketReader shutdown()"); @@ -517,14 +503,9 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { setWasAuthenticated(); - try { - boolean readerAndWriterThreadsTermianted = waitForCondition(() -> !packetWriter.running && !packetReader.running); - if (!readerAndWriterThreadsTermianted) { - LOGGER.severe("Reader and writer threads did not terminate"); - } - } catch (InterruptedException e) { - LOGGER.log(Level.FINE, "Interrupted while waiting for reader and writer threads to terminate", e); - } + // Wait for reader and writer threads to be terminated. + readerWriterSemaphore.acquireUninterruptibly(2); + readerWriterSemaphore.release(2); if (disconnectedButResumeable) { return; @@ -552,6 +533,15 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { initState(); } + @Override + protected void initState() { + super.initState(); + maybeCompressFeaturesReceived.init(); + compressSyncPoint.init(); + smResumedSyncPoint.init(); + smEnabledSyncPoint.init(); + } + @Override public void sendNonza(Nonza element) throws NotConnectedException, InterruptedException { packetWriter.sendStreamElement(element); @@ -662,6 +652,15 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { // Set the reader and writer instance variables initReaderAndWriter(); + 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 @@ -689,11 +688,17 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { * existing plain connection and perform a handshake. This method won't return until the * connection has finished the handshake or an error occurred while securing the connection. * @throws IOException if an I/O error occurred. - * @throws SecurityNotPossibleException if TLS is not possible. - * @throws CertificateException if there is an issue with the certificate. + * @throws CertificateException + * @throws NoSuchAlgorithmException if no such algorithm is available. + * @throws NoSuchProviderException + * @throws KeyStoreException + * @throws UnrecoverableKeyException + * @throws KeyManagementException if there was a key mangement error. + * @throws SmackException if Smack detected an exceptional situation. + * @throws Exception if an exception occurs. */ @SuppressWarnings("LiteralClassName") - private void proceedTLSReceived() throws IOException, SecurityNotPossibleException, CertificateException { + private void proceedTLSReceived() throws NoSuchAlgorithmException, CertificateException, IOException, KeyStoreException, NoSuchProviderException, UnrecoverableKeyException, KeyManagementException, SmackException { SmackTlsContext smackTlsContext = getSmackTlsContext(); Socket plain = socket; @@ -768,7 +773,7 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { @Override public boolean isUsingCompression() { - return compressionHandler != null && compressSyncPoint; + return compressionHandler != null && compressSyncPoint.wasSuccessful(); } /** @@ -786,10 +791,10 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { * * @throws NotConnectedException if the XMPP connection is not connected. * @throws SmackException if Smack detected an exceptional situation. + * @throws NoResponseException if there was no response from the remote entity. * @throws InterruptedException if the calling thread was interrupted. - * @throws XMPPException if an XMPP protocol error was received. */ - private void maybeEnableCompression() throws SmackException, InterruptedException, XMPPException { + private void maybeEnableCompression() throws SmackException, InterruptedException { if (!config.isCompressionEnabled()) { return; } @@ -802,9 +807,7 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { // If stream compression was offered by the server and we want to use // compression then send compression request to the server if ((compressionHandler = maybeGetCompressionHandler(compression)) != null) { - compressSyncPoint = false; - sendNonza(new Compress(compressionHandler.getCompressionMethod())); - waitForConditionOrThrowConnectionException(() -> compressSyncPoint, "establishing stream compression"); + compressSyncPoint.sendAndWaitForResponseOrThrow(new Compress(compressionHandler.getCompressionMethod())); } else { LOGGER.warning("Could not enable compression because no matching handler/method pair was found"); } @@ -832,11 +835,11 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { // We connected successfully to the servers TCP port initConnection(); - // TLS handled will be true either if TLS was established, or if it was not mandatory. - waitForConditionOrThrowConnectionException(() -> tlsHandled, "establishing TLS"); + // TLS handled will be successful either if TLS was established, or if it was not mandatory. + tlsHandled.checkIfSuccessOrWaitOrThrow(); // Wait with SASL auth until the SASL mechanisms have been received - waitForConditionOrThrowConnectionException(() -> saslFeatureReceived, "SASL mechanisms stream feature from server"); + saslFeatureReceived.checkIfSuccessOrWaitOrThrow(); } /** @@ -854,28 +857,24 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { if (startTlsFeature != null) { if (startTlsFeature.required() && config.getSecurityMode() == SecurityMode.disabled) { SecurityRequiredByServerException smackException = new SecurityRequiredByServerException(); - currentSmackException = smackException; - notifyWaitingThreads(); + tlsHandled.reportFailure(smackException); throw smackException; } if (config.getSecurityMode() != ConnectionConfiguration.SecurityMode.disabled) { sendNonza(new StartTls()); } else { - tlsHandled = true; - notifyWaitingThreads(); + tlsHandled.reportSuccess(); } } else { - tlsHandled = true; - notifyWaitingThreads(); + tlsHandled.reportSuccess(); } if (isSaslAuthenticated()) { // If we have received features after the SASL has been successfully completed, then we // have also *maybe* received, as it is an optional feature, the compression feature // from the server. - streamFeaturesAfterAuthenticationReceived = true; - notifyWaitingThreads(); + maybeCompressFeaturesReceived.reportSuccess(); } } @@ -900,8 +899,6 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { private volatile boolean done; - private boolean running; - /** * Initializes the reader in order to be used. The reader is initialized during the * first connection and when reconnecting due to an abruptly disconnection. @@ -913,13 +910,11 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { @Override public void run() { LOGGER.finer(threadName + " start"); - running = true; try { parsePackets(); } finally { LOGGER.finer(threadName + " exit"); - running = false; - notifyWaitingThreads(); + XMPPTCPConnection.this.readerWriterSemaphore.release(); } } }, threadName); @@ -936,8 +931,10 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { * Parse top-level packets in order to process them further. */ private void parsePackets() { + boolean initialStreamOpenSend = false; try { openStreamAndResetParser(); + initialStreamOpenSend = true; XmlPullParser.Event eventType = parser.getEventType(); while (!done) { switch (eventType) { @@ -958,17 +955,27 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { break; case "error": StreamError streamError = PacketParserUtils.parseStreamError(parser); - // Stream errors are non recoverable, throw this exceptions. Also note that this will set - // this exception as current connection exceptions and notify any waiting threads. + saslFeatureReceived.reportFailure(new StreamErrorException(streamError)); + // Mark the tlsHandled sync point as success, we will use the saslFeatureReceived sync + // point to report the error, which is checked immediately after tlsHandled in + // connectInternal(). + tlsHandled.reportSuccess(); throw new StreamErrorException(streamError); case "features": parseFeaturesAndNotify(parser); break; case "proceed": - // Secure the connection by negotiating TLS - proceedTLSReceived(); - // Send a new opening stream to the server - openStreamAndResetParser(); + try { + // Secure the connection by negotiating TLS + proceedTLSReceived(); + // Send a new opening stream to the server + openStreamAndResetParser(); + } + catch (Exception e) { + SmackException.SmackWrappedException smackException = new SmackException.SmackWrappedException(e); + tlsHandled.reportFailure(smackException); + throw e; + } break; case "failure": String namespace = parser.getNamespace(null); @@ -982,8 +989,8 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { // situation. It is still possible to authenticate and // use the connection but using an uncompressed connection // TODO Parse failure stanza - currentSmackException = new SmackException.SmackMessageException("Could not establish compression"); - notifyWaitingThreads(); + compressSyncPoint.reportFailure(new SmackException.SmackMessageException( + "Could not establish compression")); break; default: parseAndProcessNonza(parser); @@ -997,8 +1004,7 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { // Send a new opening stream to the server openStreamAndResetParser(); // Notify that compression is being used - compressSyncPoint = true; - notifyWaitingThreads(); + compressSyncPoint.reportSuccess(); break; case Enabled.ELEMENT: Enabled enabled = ParseStreamManagement.enabled(parser); @@ -1006,7 +1012,7 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { smSessionId = enabled.getId(); if (StringUtils.isNullOrEmpty(smSessionId)) { SmackException xmppException = new SmackException.SmackMessageException("Stream Management 'enabled' element with resume attribute but without session id received"); - setCurrentConnectionExceptionAndNotify(xmppException); + smEnabledSyncPoint.reportFailure(xmppException); throw xmppException; } smServerMaxResumptionTime = enabled.getMaxResumptionTime(); @@ -1016,19 +1022,28 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { } clientHandledStanzasCount = 0; smWasEnabledAtLeastOnce = true; - smEnabledSyncPoint = true; - notifyWaitingThreads(); + smEnabledSyncPoint.reportSuccess(); + LOGGER.fine("Stream Management (XEP-198): successfully enabled"); break; case Failed.ELEMENT: Failed failed = ParseStreamManagement.failed(parser); - if (smResumedSyncPoint == SyncPointState.request_sent) { - // This is a nonza in a response to resuming a previous stream, failure to do - // so is non-fatal as we can simply continue with resource binding in this case. - smResumptionFailed = failed; - notifyWaitingThreads(); - } else { - FailedNonzaException xmppException = new FailedNonzaException(failed, failed.getStanzaErrorCondition()); - setCurrentConnectionExceptionAndNotify(xmppException); + FailedNonzaException xmppException = new FailedNonzaException(failed, failed.getStanzaErrorCondition()); + // If only XEP-198 would specify different failure elements for the SM + // enable and SM resume failure case. But this is not the case, so we + // need to determine if this is a 'Failed' response for either 'Enable' + // or 'Resume'. + if (smResumedSyncPoint.requestSent()) { + smResumedSyncPoint.reportFailure(xmppException); + } + else { + if (!smEnabledSyncPoint.requestSent()) { + throw new IllegalStateException("Failed element received but SM was not previously enabled"); + } + smEnabledSyncPoint.reportFailure(new SmackException.SmackWrappedException(xmppException)); + // Report success for last lastFeaturesReceived so that in case a + // failed resumption, we can continue with normal resource binding. + // See text of XEP-198 5. below Example 11. + lastFeaturesReceived.reportSuccess(); } break; case Resumed.ELEMENT: @@ -1037,7 +1052,7 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { throw new StreamIdDoesNotMatchException(smSessionId, resumed.getPrevId()); } // Mark SM as enabled - smEnabledSyncPoint = true; + smEnabledSyncPoint.reportSuccess(); // First, drop the stanzas already handled by the server processHandledCount(resumed.getHandledCount()); // Then re-send what is left in the unacknowledged queue @@ -1053,8 +1068,8 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { requestSmAcknowledgementInternal(); } // Mark SM resumption as successful - smResumedSyncPoint = SyncPointState.successful; - notifyWaitingThreads(); + smResumedSyncPoint.reportSuccess(); + LOGGER.fine("Stream Management (XEP-198): Stream resumed"); break; case AckAnswer.ELEMENT: AckAnswer ackAnswer = ParseStreamManagement.ackAnswer(parser); @@ -1062,7 +1077,7 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { break; case AckRequest.ELEMENT: ParseStreamManagement.ackRequest(parser); - if (smEnabledSyncPoint) { + if (smEnabledSyncPoint.wasSuccessful()) { sendSmAcknowledgementInternal(); } else { LOGGER.warning("SM Ack Request received while SM is not enabled"); @@ -1086,8 +1101,7 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { // did re-start the queue again, causing this writer to assume that the queue is not // shutdown, which results in a call to disconnect(). final boolean queueWasShutdown = packetWriter.queue.isShutdown(); - closingStreamReceived = true; - notifyWaitingThreads(); + closingStreamReceived.reportSuccess(); if (queueWasShutdown) { // We received a closing stream element *after* we initiated the @@ -1123,9 +1137,12 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { } } catch (Exception e) { + // TODO: Move the call closingStreamReceived.reportFailure(e) into notifyConnectionError? + closingStreamReceived.reportFailure(e); // The exception can be ignored if the the connection is 'done' - // or if the it was caused because the socket got closed. - if (!done) { + // or if the it was caused because the socket got closed. It can not be ignored if it + // happened before (or while) the initial stream opened was send. + if (!(done || packetWriter.queue.isShutdown()) || !initialStreamOpenSend) { // Close the connection and notify connection listeners of the // error. notifyConnectionError(e); @@ -1144,6 +1161,12 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { private final ArrayBlockingQueueWithShutdown queue = new ArrayBlockingQueueWithShutdown<>( QUEUE_SIZE, true); + /** + * Needs to be protected for unit testing purposes. + */ + protected SynchronizationPoint shutdownDone = new SynchronizationPoint<>( + XMPPTCPConnection.this, "shutdown completed"); + /** * If set, the stanza writer is shut down */ @@ -1161,13 +1184,12 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { */ private boolean shouldBundleAndDefer; - private boolean running; - /** * Initializes the writer in order to be used. It is called at the first connection and also * is invoked if the connection is disconnected by an error. */ void init() { + shutdownDone.init(); shutdownTimestamp = null; if (unacknowledgedStanzas != null) { @@ -1182,13 +1204,11 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { @Override public void run() { LOGGER.finer(threadName + " start"); - running = true; try { writePackets(); } finally { LOGGER.finer(threadName + " exit"); - running = false; - notifyWaitingThreads(); + XMPPTCPConnection.this.readerWriterSemaphore.release(); } } }, threadName); @@ -1236,11 +1256,19 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { /** * Shuts down the stanza writer. Once this method has been called, no further * packets will be written to the server. + * @throws InterruptedException if the calling thread was interrupted. */ void shutdown(boolean instant) { instantShutdown = instant; queue.shutdown(); shutdownTimestamp = System.currentTimeMillis(); + 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); + } + } } /** @@ -1342,6 +1370,7 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { } writer.write(packet.toXML().toString()); } + writer.flush(); } catch (Exception e) { LOGGER.log(Level.WARNING, @@ -1378,6 +1407,9 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { } else { LOGGER.log(Level.FINE, "Ignoring Exception in writePackets()", e); } + } finally { + LOGGER.fine("Reporting shutdownDone success in writer thread"); + shutdownDone.reportSuccess(); } // Delay notifyConnectionError after shutdownDone has been reported in the finally block. if (writerException != null) { @@ -1689,7 +1721,7 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { * @return true if Stream Management was negotiated. */ public boolean isSmEnabled() { - return smEnabledSyncPoint; + return smEnabledSyncPoint.wasSuccessful(); } /** @@ -1698,7 +1730,7 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { * @return true if the stream was resumed. */ public boolean streamWasResumed() { - return smResumedSyncPoint == SyncPointState.successful; + return smResumedSyncPoint.wasSuccessful(); } /** diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XMPPTCPConnectionConfiguration.java b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XMPPTCPConnectionConfiguration.java index e4bb64217..aaccfacb6 100644 --- a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XMPPTCPConnectionConfiguration.java +++ b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XMPPTCPConnectionConfiguration.java @@ -26,7 +26,7 @@ import org.jivesoftware.smack.ConnectionConfiguration; *

*
  * {@code
- * XMPPTCPConnectionConfiguration conf = XMPPTCPConnectionConfiguration.builder()
+ * XMPPTCPConnectionConfiguration conf = XMPPConnectionConfiguration.builder()
  *     .setXmppDomain("example.org").setUsernameAndPassword("user", "password")
  *     .setCompressionEnabled(false).build();
  * XMPPTCPConnection connection = new XMPPTCPConnection(conf);
diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XmppTcpTransportModule.java b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XmppTcpTransportModule.java
index e48c35b25..74286fdd9 100644
--- a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XmppTcpTransportModule.java
+++ b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XmppTcpTransportModule.java
@@ -47,13 +47,17 @@ import javax.net.ssl.SSLSession;
 
 import org.jivesoftware.smack.ConnectionConfiguration.SecurityMode;
 import org.jivesoftware.smack.SmackException;
+import org.jivesoftware.smack.SmackException.ConnectionException;
+import org.jivesoftware.smack.SmackException.ConnectionUnexpectedTerminatedException;
+import org.jivesoftware.smack.SmackException.NoResponseException;
+import org.jivesoftware.smack.SmackException.NotConnectedException;
 import org.jivesoftware.smack.SmackException.SecurityRequiredByClientException;
 import org.jivesoftware.smack.SmackException.SecurityRequiredByServerException;
-import org.jivesoftware.smack.SmackException.SmackCertificateException;
+import org.jivesoftware.smack.SmackException.SmackWrappedException;
 import org.jivesoftware.smack.SmackFuture;
 import org.jivesoftware.smack.SmackFuture.InternalSmackFuture;
 import org.jivesoftware.smack.SmackReactor.SelectionKeyAttachment;
-import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.XMPPException.FailedNonzaException;
 import org.jivesoftware.smack.XmppInputOutputFilter;
 import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnection.ConnectedButUnauthenticatedStateDescriptor;
 import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnection.LookupRemoteConnectionEndpointsStateDescriptor;
@@ -587,7 +591,7 @@ public class XmppTcpTransportModule extends ModularXmppClientToServerConnectionM
 
         @Override
         protected List> lookupConnectionEndpoints() {
-            // Assert that there are no stale discovered endpoints prior performing the lookup.
+            // Assert that there are no stale discovred endpoints prior performing the lookup.
             assert discoveredTcpEndpoints == null;
 
             List> futures = new ArrayList<>(2);
@@ -740,14 +744,23 @@ public class XmppTcpTransportModule extends ModularXmppClientToServerConnectionM
 
         @Override
         public StateTransitionResult.AttemptResult transitionInto(WalkStateGraphContext walkStateGraphContext)
-                        throws InterruptedException, IOException, SmackException, XMPPException {
+                        throws InterruptedException, ConnectionUnexpectedTerminatedException, NotConnectedException,
+                        NoResponseException, IOException {
             // The fields inetSocketAddress and failedAddresses are handed over from LookupHostAddresses to
             // ConnectingToHost.
             ConnectionAttemptState connectionAttemptState = new ConnectionAttemptState(connectionInternal, discoveredTcpEndpoints,
                     this);
-            StateTransitionResult.Failure failure = connectionAttemptState.establishTcpConnection();
-            if (failure != null) {
-                return failure;
+            connectionAttemptState.establishTcpConnection();
+
+            try {
+                connectionAttemptState.tcpConnectionEstablishedSyncPoint.checkIfSuccessOrWaitOrThrow();
+            } catch (ConnectionException | NoResponseException e) {
+                // TODO: It is not really elegant that we catch the exception here. Ideally ConnectionAttemptState would
+                // simply return a StateTranstionResult.FailureCausedByException.
+                return new StateTransitionResult.FailureCausedByException<>(e);
+            } catch (SmackWrappedException e) {
+                // Should never throw SmackWrappedException.
+                throw new AssertionError(e);
             }
 
             socketChannel = connectionAttemptState.socketChannel;
@@ -845,7 +858,8 @@ public class XmppTcpTransportModule extends ModularXmppClientToServerConnectionM
 
         @Override
         public StateTransitionResult.AttemptResult transitionInto(WalkStateGraphContext walkStateGraphContext)
-                        throws IOException, InterruptedException, SmackException, XMPPException {
+                        throws SmackWrappedException, FailedNonzaException, IOException, InterruptedException,
+                        ConnectionUnexpectedTerminatedException, NoResponseException, NotConnectedException {
             connectionInternal.sendAndWaitForResponse(StartTls.INSTANCE, TlsProceed.class, TlsFailure.class);
 
             SmackTlsContext smackTlsContext = connectionInternal.getSmackTlsContext();
@@ -867,7 +881,7 @@ public class XmppTcpTransportModule extends ModularXmppClientToServerConnectionM
             try {
                 tlsState.waitForHandshakeFinished();
             } catch (CertificateException e) {
-                throw new SmackCertificateException(e);
+                throw new SmackWrappedException(e);
             }
 
             connectionInternal.newStreamOpenWaitForFeaturesSequence("stream features after TLS established");
@@ -1152,20 +1166,43 @@ public class XmppTcpTransportModule extends ModularXmppClientToServerConnectionM
         private void handleSslException(SSLException e) {
             handshakeException = e;
             handshakeStatus = TlsHandshakeStatus.failed;
-            connectionInternal.notifyWaitingThreads();
+            synchronized (this) {
+                notifyAll();
+            }
         }
 
         private void onHandshakeFinished() {
             handshakeStatus = TlsHandshakeStatus.successful;
-            connectionInternal.notifyWaitingThreads();
+            synchronized (this) {
+                notifyAll();
+            }
         }
 
         private boolean isHandshakeFinished() {
             return handshakeStatus == TlsHandshakeStatus.successful || handshakeStatus == TlsHandshakeStatus.failed;
         }
 
-        private void waitForHandshakeFinished() throws InterruptedException, CertificateException, SSLException, SmackException, XMPPException {
-            connectionInternal.waitForCondition(() -> isHandshakeFinished(), "TLS handshake to finish");
+        private void waitForHandshakeFinished() throws InterruptedException, CertificateException, SSLException, ConnectionUnexpectedTerminatedException, NoResponseException {
+            final long deadline = System.currentTimeMillis() + connectionInternal.connection.getReplyTimeout();
+
+            Exception currentConnectionException = null;
+            synchronized (this) {
+                while (!isHandshakeFinished()
+                                && (currentConnectionException = connectionInternal.getCurrentConnectionException()) == null) {
+                    final long now = System.currentTimeMillis();
+                    if (now >= deadline)
+                        break;
+                    wait(deadline - now);
+                }
+            }
+
+            if (currentConnectionException != null) {
+                throw new SmackException.ConnectionUnexpectedTerminatedException(currentConnectionException);
+            }
+
+            if (!isHandshakeFinished()) {
+                throw NoResponseException.newWith(connectionInternal.connection, "TLS handshake to finish");
+            }
 
             if (handshakeStatus == TlsHandshakeStatus.failed) {
                 throw handshakeException;
@@ -1198,7 +1235,7 @@ public class XmppTcpTransportModule extends ModularXmppClientToServerConnectionM
 
         @Override
         public void waitUntilInputOutputClosed() throws IOException, CertificateException, InterruptedException,
-                SmackException, XMPPException {
+                ConnectionUnexpectedTerminatedException, NoResponseException {
             waitForHandshakeFinished();
         }
 
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 b6a2ab6a9..95f74bc60 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-2020 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.
@@ -124,6 +124,8 @@ public class PacketWriterTest {
         // Not really cool, but may increases the chances for 't' to block in sendStanza.
         Thread.sleep(250);
 
+        // Set to true for testing purposes, so that shutdown() won't wait packet writer
+        pw.shutdownDone.reportSuccess();
         // Shutdown the packetwriter, this will also interrupt the writer thread, which is what we hope to happen in the
         // thread created above.
         pw.shutdown(false);
diff --git a/version b/version
index c4b97782d..92560b19f 100644
--- a/version
+++ b/version
@@ -1 +1 @@
-4.4.0-alpha5-SNAPSHOT
+4.4.0-alpha4-SNAPSHOT