From e98d42790acdb9440694af8153a7b679216f7d42 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Mon, 4 Feb 2019 08:59:39 +0100 Subject: [PATCH] SmackReactor/NIO, Java8/Android19, Pretty print XML, FSM connections This commit adds - SmackReactor / NIO - a framework for finite state machine connections - support for Java 8 - pretty printed XML debug output It also - reworks the integration test framework - raises the minimum Android API level to 19 - introduces XmppNioTcpConnection Furthermore fixes SMACK-801 (at least partly). Java 8 language features are available, but not all runtime library methods. For that we would need to raise the Android API level to 24 or higher. --- build.gradle | 40 +- resources/eclipse/smack_formatter.xml | 582 ++--- .../smack/bosh/XMPPBOSHConnection.java | 12 - .../smack/AbstractXMPPConnection.java | 366 +++- .../smack/AbstractXmppNioConnection.java | 54 + .../smack/GenericElementListener.java | 36 + .../java/org/jivesoftware/smack/Manager.java | 5 +- .../org/jivesoftware/smack/NonzaCallback.java | 176 ++ .../smack/SASLAuthentication.java | 5 +- .../jivesoftware/smack/ScheduledAction.java | 66 + .../jivesoftware/smack/SmackException.java | 26 +- .../smack/SmackInitialization.java | 13 + .../org/jivesoftware/smack/SmackReactor.java | 440 ++++ .../jivesoftware/smack/StanzaCollector.java | 96 +- .../smack/SynchronizationPoint.java | 23 +- .../jivesoftware/smack/XMPPConnection.java | 4 +- .../org/jivesoftware/smack/XMPPException.java | 4 + .../smack/XmppInputOutputFilter.java | 76 + .../smack/compress/packet/Compress.java | 2 +- .../smack/compress/packet/Failure.java | 89 + .../compress/provider/CompressedProvider.java | 36 + .../compress/provider/FailureProvider.java | 81 + .../smack/compress/provider/package-info.java | 21 + .../compression/XMPPInputOutputStream.java | 6 +- .../compression/XmppCompressionFactory.java | 47 + .../compression/XmppCompressionManager.java | 65 + .../zlib/ZlibXmppCompressionFactory.java | 287 +++ .../smack/compression/zlib/package-info.java | 21 + .../smack/debugger/AbstractDebugger.java | 18 +- .../smack/debugger/SmackDebugger.java | 59 +- .../AbstractXmppStateMachineConnection.java | 816 ++++++++ .../smack/fsm/ConnectionStateEvent.java | 113 + .../fsm/ConnectionStateMachineListener.java | 24 + .../jivesoftware/smack/fsm/LoginContext.java | 33 + .../smack/fsm/StateDescriptor.java | 221 ++ .../smack/fsm/StateDescriptorGraph.java | 419 ++++ .../smack/fsm/StateMachineException.java | 60 + .../jivesoftware/smack/fsm/package-info.java | 21 + .../smack/packet/ExtensionElement.java | 12 +- .../smack/packet/FullyQualifiedElement.java | 28 + .../org/jivesoftware/smack/packet/Nonza.java | 2 +- .../jivesoftware/smack/packet/StartTls.java | 6 +- .../smack/packet/StreamClose.java | 42 + .../jivesoftware/smack/packet/TlsFailure.java | 43 + .../jivesoftware/smack/packet/TlsProceed.java | 43 + .../smack/packet/TopLevelStreamElement.java | 3 +- .../smack/provider/NonzaProvider.java | 24 + .../jivesoftware/smack/provider/Provider.java | 23 + .../smack/provider/ProviderManager.java | 28 + .../smack/provider/TlsFailureProvider.java | 35 + .../smack/provider/TlsProceedProvider.java | 35 + .../util/ArrayBlockingQueueWithShutdown.java | 13 + .../org/jivesoftware/smack/util/Async.java | 2 +- .../smack/util/CollectionUtil.java | 21 +- .../org/jivesoftware/smack/util/MultiMap.java | 29 +- .../smack/util/PacketParserUtils.java | 1 + .../jivesoftware/smack/util/StringUtils.java | 2 +- .../org/jivesoftware/smack/util/UTF8.java | 39 + .../smack/util/XmlStringBuilder.java | 10 +- .../smack/util/XmppElementUtil.java | 45 + .../smack/util/dns/HostAddress.java | 9 + .../smack/util/dns/SmackDaneVerifier.java | 6 +- .../jivesoftware/smack/DummyConnection.java | 4 +- .../smack/StanzaCollectorTest.java | 85 +- .../smack/compress/packet/FailureTest.java | 58 + .../provider/FailureProviderTest.java | 56 + .../debugger/slf4j/SLF4JSmackDebugger.java | 16 +- .../smackx/debugger/EnhancedDebugger.java | 16 +- .../smackx/debugger/LiteDebugger.java | 16 +- .../gcm/provider/GcmExtensionProvider.java | 7 +- .../AbstractJsonExtensionProvider.java | 8 +- .../json/provider/JsonExtensionProvider.java | 7 +- .../jivesoftware/smackx/ping/PingManager.java | 71 +- .../smack/roster/RosterVersioningTest.java | 4 +- smack-integration-test/build.gradle | 2 +- .../smack/XmppConnectionStressTest.java | 266 +++ .../smack/inttest/AbstractSmackIntTest.java | 10 +- .../inttest/AbstractSmackIntegrationTest.java | 18 +- .../AbstractSmackLowLevelIntegrationTest.java | 58 +- ...tSmackSpecificLowLevelIntegrationTest.java | 63 + .../smack/inttest/Configuration.java | 35 +- ...ConnectionConfigurationBuilderApplier.java | 23 + .../smack/inttest/FailedTest.java | 7 +- .../smack/inttest/IntTestUtil.java | 241 --- .../smack/inttest/SmackIntegrationTest.java | 8 +- .../SmackIntegrationTestEnvironment.java | 19 +- .../SmackIntegrationTestFramework.java | 716 ++++--- .../smack/inttest/SuccessfulTest.java | 7 +- .../smack/inttest/TestNotPossible.java | 7 +- .../smack/inttest/TestResult.java | 9 +- .../inttest/XmppConnectionDescriptor.java | 104 + .../smack/inttest/XmppConnectionManager.java | 441 ++++ .../java/org/jivesoftware/smack/ChatTest.java | 4 +- .../smack/LoginIntegrationTest.java | 13 +- .../smack/StreamManagementTest.java | 23 +- .../WaitForClosingStreamElementTest.java | 10 +- .../smack/XmppConnectionIntegrationTest.java | 67 + .../chat2/AbstractChatIntegrationTest.java | 2 +- ...ncomingMessageListenerIntegrationTest.java | 2 +- ...utgoingMessageListenerIntegrationTest.java | 2 +- .../roster/LowLevelRosterIntegrationTest.java | 9 +- .../smack/roster/RosterIntegrationTest.java | 4 +- ...oTcpConnectionLowLevelIntegrationTest.java | 46 + .../jivesoftware/smack/tcp/package-info.java | 21 + .../smackx/caps/EntityCapsTest.java | 4 +- .../chatstate/ChatStateIntegrationTest.java | 2 +- .../FileTransferIntegrationTest.java | 4 +- .../HttpFileUploadIntegrationTest.java | 4 +- .../smackx/iot/IoTControlIntegrationTest.java | 4 +- .../smackx/iot/IoTDataIntegrationTest.java | 4 +- .../iot/IoTDiscoveryIntegrationTest.java | 4 +- .../iqversion/VersionIntegrationTest.java | 4 +- .../smackx/mam/MamIntegrationTest.java | 4 +- .../smackx/mood/MoodIntegrationTest.java | 2 +- .../muc/MultiUserChatIntegrationTest.java | 4 +- .../MultiUserChatLowLevelIntegrationTest.java | 22 +- .../omemo/AbstractOmemoIntegrationTest.java | 4 +- .../AbstractTwoUsersOmemoIntegrationTest.java | 2 +- .../MessageEncryptionIntegrationTest.java | 2 +- .../smackx/omemo/OmemoMamDecryptionTest.java | 2 +- .../omemo/ReadOnlyDeviceIntegrationTest.java | 2 +- .../SessionRenegotiationIntegrationTest.java | 2 +- .../ox/AbstractOpenPgpIntegrationTest.java | 2 +- .../ox/OXSecretKeyBackupIntegrationTest.java | 2 +- .../OXInstantMessagingIntegrationTest.java | 2 +- .../smackx/ping/PingIntegrationTest.java | 4 +- .../smackx/pubsub/PubSubIntegrationTest.java | 4 +- .../jivesoftware/smackx/xdata/FormTest.java | 4 +- .../DummySmackIntegrationTestFramework.java | 40 +- .../SmackIntegrationTestFrameWorkTest.java | 96 + .../SmackIntegrationTestUnitTestUtil.java | 12 +- ...egrationTestXmppConnectionManagerTest.java | 45 + ...SmackIntegrationTestFrameworkUnitTest.java | 28 +- ...ckOmemoSignalIntegrationTestFramework.java | 6 +- smack-repl/build.gradle | 5 + .../igniterealtime/smack/smackrepl/Nio.java | 109 + .../smack/smackrepl/StateGraph.java | 44 + .../util/dns/minidns/MiniDnsDaneVerifier.java | 14 + smack-tcp/Makefile | 12 + .../smack/tcp/doc-files/.gitignore | 1 + .../smack/tcp/XMPPTCPConnection.java | 193 +- .../smack/tcp/XmppNioTcpConnection.java | 1865 +++++++++++++++++ .../smack/tcp/XmppNioTcpConnectionTest.java | 32 + version.gradle | 2 +- 144 files changed, 8692 insertions(+), 1455 deletions(-) create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/AbstractXmppNioConnection.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/GenericElementListener.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/NonzaCallback.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/ScheduledAction.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/SmackReactor.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/XmppInputOutputFilter.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/compress/packet/Failure.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/compress/provider/CompressedProvider.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/compress/provider/FailureProvider.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/compress/provider/package-info.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/compression/XmppCompressionFactory.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/compression/XmppCompressionManager.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/compression/zlib/ZlibXmppCompressionFactory.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/compression/zlib/package-info.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/fsm/AbstractXmppStateMachineConnection.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/fsm/ConnectionStateEvent.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/fsm/ConnectionStateMachineListener.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/fsm/LoginContext.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/fsm/StateDescriptor.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/fsm/StateDescriptorGraph.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/fsm/StateMachineException.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/fsm/package-info.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/packet/FullyQualifiedElement.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/packet/StreamClose.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/packet/TlsFailure.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/packet/TlsProceed.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/provider/NonzaProvider.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/provider/TlsFailureProvider.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/provider/TlsProceedProvider.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/util/UTF8.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/util/XmppElementUtil.java create mode 100644 smack-core/src/test/java/org/jivesoftware/smack/compress/packet/FailureTest.java create mode 100644 smack-core/src/test/java/org/jivesoftware/smack/compress/provider/FailureProviderTest.java create mode 100644 smack-integration-test/src/main/java/org/igniterealtime/smack/XmppConnectionStressTest.java create mode 100644 smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/AbstractSmackSpecificLowLevelIntegrationTest.java create mode 100644 smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/ConnectionConfigurationBuilderApplier.java delete mode 100644 smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/IntTestUtil.java create mode 100644 smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/XmppConnectionDescriptor.java create mode 100644 smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/XmppConnectionManager.java create mode 100644 smack-integration-test/src/main/java/org/jivesoftware/smack/XmppConnectionIntegrationTest.java create mode 100644 smack-integration-test/src/main/java/org/jivesoftware/smack/tcp/XmppNioTcpConnectionLowLevelIntegrationTest.java create mode 100644 smack-integration-test/src/main/java/org/jivesoftware/smack/tcp/package-info.java create mode 100644 smack-integration-test/src/test/java/org/igniterealtime/smack/inttest/SmackIntegrationTestFrameWorkTest.java create mode 100644 smack-integration-test/src/test/java/org/igniterealtime/smack/inttest/SmackIntegrationTestXmppConnectionManagerTest.java create mode 100644 smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/Nio.java create mode 100644 smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/StateGraph.java create mode 100644 smack-tcp/Makefile create mode 100644 smack-tcp/src/javadoc/org/jivesoftware/smack/tcp/doc-files/.gitignore create mode 100644 smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XmppNioTcpConnection.java create mode 100644 smack-tcp/src/test/java/org/jivesoftware/smack/tcp/XmppNioTcpConnectionTest.java diff --git a/build.gradle b/build.gradle index 504d07aeb..80589f188 100644 --- a/build.gradle +++ b/build.gradle @@ -102,7 +102,7 @@ allprojects { junitVersion = '5.2.0' } group = 'org.igniterealtime.smack' - sourceCompatibility = 1.7 + sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = sourceCompatibility version = shortVersion if (isSnapshot) { @@ -245,7 +245,14 @@ gradle.taskGraph.whenReady { taskGraph -> } } -task javadocAll(type: Javadoc) { +task copyAllJavadocDocFiles(type: Copy) { + from javadocAllProjects.collect { project -> + "${project.projectDir}/src/javadoc" } + into javadocAllDir + include '**/doc-files/*.*' +} + +task javadocAll(type: Javadoc, dependsOn: copyAllJavadocDocFiles) { source javadocAllProjects.collect {project -> project.sourceSets.main.allJava } destinationDir = javadocAllDir @@ -449,12 +456,39 @@ subprojects { enabled false semver false } + + // Work around https://github.com/gradle/gradle/issues/4046 + javadoc.dependsOn('copyJavadocDocFiles') + task copyJavadocDocFiles(type: Copy) { + from('src/javadoc') + into 'build/docs/javadoc' + include '**/doc-files/*.*' + } + + // If this subproject has a Makefile then make copyJavadocDocFiles + // and the root project's javadocAll task dependend on + // generateFiles. + if (file("$projectDir/Makefile").exists()) { + copyJavadocDocFiles.dependsOn('generateFiles') + rootProject.copyAllJavadocDocFiles.dependsOn("${project.name}:generateFiles") + task generateFiles(type: Exec) { + workingDir projectDir + commandLine 'make' + } + + clean.dependsOn('cleanGeneratedFiles') + rootProject.clean.dependsOn("${project.name}:cleanGeneratedFiles") + task cleanGeneratedFiles(type: Exec) { + workingDir projectDir + commandLine 'make', 'clean' + } + } } configure (androidProjects + androidBootClasspathProjects) { apply plugin: 'ru.vyarus.animalsniffer' dependencies { - signature "net.sf.androidscents.signature:android-api-level-${smackMinAndroidSdk}:2.3.1_r2@signature" + signature "net.sf.androidscents.signature:android-api-level-${smackMinAndroidSdk}:4.4.2_r4@signature" } animalsniffer { sourceSets = [sourceSets.main] diff --git a/resources/eclipse/smack_formatter.xml b/resources/eclipse/smack_formatter.xml index a2ae6a5a3..9c59b8b32 100644 --- a/resources/eclipse/smack_formatter.xml +++ b/resources/eclipse/smack_formatter.xml @@ -1,295 +1,315 @@ - - - - - - - - - - - - - - - - - - - - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - + + + + + + + + + + - - - - - - - + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/smack-bosh/src/main/java/org/jivesoftware/smack/bosh/XMPPBOSHConnection.java b/smack-bosh/src/main/java/org/jivesoftware/smack/bosh/XMPPBOSHConnection.java index 552cba9c2..3446c957d 100644 --- a/smack-bosh/src/main/java/org/jivesoftware/smack/bosh/XMPPBOSHConnection.java +++ b/smack-bosh/src/main/java/org/jivesoftware/smack/bosh/XMPPBOSHConnection.java @@ -387,18 +387,6 @@ public class XMPPBOSHConnection extends AbstractXMPPConnection { readerConsumer.start(); } - /** - * Sends out a notification that there was an error with the connection - * and closes the connection. - * - * @param e the exception that causes the connection close event. - */ - protected void notifyConnectionError(Exception e) { - // Closes the connection temporary. A reconnection is possible - shutdown(); - callConnectionClosedOnErrorListener(e); - } - /** * A listener class which listen for a successfully established connection * and connection errors and notifies the BOSHConnection. 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 7967bdc12..75b1c297b 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/AbstractXMPPConnection.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/AbstractXMPPConnection.java @@ -1,6 +1,6 @@ /** * - * Copyright 2009 Jive Software. + * Copyright 2009 Jive Software, 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,9 +16,23 @@ */ package org.jivesoftware.smack; +import java.io.ByteArrayInputStream; +import java.io.FileInputStream; import java.io.IOException; +import java.io.InputStream; import java.io.Reader; import java.io.Writer; +import java.lang.reflect.Constructor; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.Provider; +import java.security.SecureRandom; +import java.security.Security; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -33,8 +47,6 @@ import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; @@ -43,6 +55,16 @@ import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.Logger; +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.PasswordCallback; + +import org.jivesoftware.smack.ConnectionConfiguration.DnssecMode; import org.jivesoftware.smack.ConnectionConfiguration.SecurityMode; import org.jivesoftware.smack.SmackConfiguration.UnknownIqRequestReplyMode; import org.jivesoftware.smack.SmackException.AlreadyConnectedException; @@ -54,6 +76,7 @@ import org.jivesoftware.smack.SmackException.ResourceBindingNotOfferedException; import org.jivesoftware.smack.SmackException.SecurityRequiredByClientException; import org.jivesoftware.smack.SmackException.SecurityRequiredException; import org.jivesoftware.smack.SmackFuture.InternalSmackFuture; +import org.jivesoftware.smack.XMPPException.FailedNonzaException; import org.jivesoftware.smack.XMPPException.StreamErrorException; import org.jivesoftware.smack.XMPPException.XMPPErrorException; import org.jivesoftware.smack.compress.packet.Compress; @@ -67,6 +90,7 @@ import org.jivesoftware.smack.iqrequest.IQRequestHandler; import org.jivesoftware.smack.packet.Bind; import org.jivesoftware.smack.packet.ErrorIQ; import org.jivesoftware.smack.packet.ExtensionElement; +import org.jivesoftware.smack.packet.FullyQualifiedElement; import org.jivesoftware.smack.packet.IQ; import org.jivesoftware.smack.packet.Mechanisms; import org.jivesoftware.smack.packet.Message; @@ -77,8 +101,11 @@ import org.jivesoftware.smack.packet.Stanza; import org.jivesoftware.smack.packet.StanzaError; import org.jivesoftware.smack.packet.StartTls; import org.jivesoftware.smack.packet.StreamError; +import org.jivesoftware.smack.packet.StreamOpen; +import org.jivesoftware.smack.packet.TopLevelStreamElement; import org.jivesoftware.smack.parsing.ParsingExceptionCallback; import org.jivesoftware.smack.provider.ExtensionElementProvider; +import org.jivesoftware.smack.provider.NonzaProvider; import org.jivesoftware.smack.provider.ProviderManager; import org.jivesoftware.smack.sasl.core.SASLAnonymous; import org.jivesoftware.smack.util.DNSUtil; @@ -87,6 +114,8 @@ import org.jivesoftware.smack.util.PacketParserUtils; import org.jivesoftware.smack.util.ParserUtils; import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.smack.util.dns.HostAddress; +import org.jivesoftware.smack.util.dns.SmackDaneProvider; +import org.jivesoftware.smack.util.dns.SmackDaneVerifier; import org.jxmpp.jid.DomainBareJid; import org.jxmpp.jid.EntityFullJid; @@ -132,6 +161,12 @@ import org.xmlpull.v1.XmlPullParser; public abstract class AbstractXMPPConnection implements XMPPConnection { private static final Logger LOGGER = Logger.getLogger(AbstractXMPPConnection.class.getName()); + protected static final SmackReactor SMACK_REACTOR; + + static { + SMACK_REACTOR = SmackReactor.getInstance(); + } + /** * Counter to uniquely identify connections that are created. */ @@ -186,9 +221,11 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { private final Map interceptors = new HashMap<>(); + final Map nonzaCallbacks = new HashMap<>(); + protected final Lock connectionLock = new ReentrantLock(); - protected final Map streamFeatures = new HashMap<>(); + protected final Map streamFeatures = new HashMap<>(); /** * The full JID of the authenticated user, as returned by the resource binding response of the server. @@ -244,6 +281,14 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { 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 final SynchronizationPoint closingStreamReceived = new SynchronizationPoint<>( + this, "stream closing element received"); + /** * The SASLAuthentication manager that is responsible for authenticating with the server. */ @@ -269,20 +314,6 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { private ParsingExceptionCallback parsingExceptionCallback = SmackConfiguration.getDefaultParsingExceptionCallback(); - /** - * This scheduled thread pool executor is used to remove pending callbacks. - */ - protected static final ScheduledExecutorService SCHEDULED_EXECUTOR_SERVICE = Executors.newSingleThreadScheduledExecutor( - new ThreadFactory() { - @Override - public Thread newThread(Runnable runnable) { - Thread thread = new Thread(runnable); - thread.setName("Smack Scheduled Executor Service"); - thread.setDaemon(true); - return thread; - } - }); - /** * A cached thread pool executor service with custom thread factory to set meaningful names on the threads and set * them 'daemon'. @@ -297,7 +328,7 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { } }); - private static final AsyncButOrdered ASYNC_BUT_ORDERED = new AsyncButOrdered<>(); + protected static final AsyncButOrdered ASYNC_BUT_ORDERED = new AsyncButOrdered<>(); /** * The used host to establish the connection to @@ -320,6 +351,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<>(); @@ -566,7 +599,7 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { return streamId; } - protected void bindResourceAndEstablishSession(Resourcepart resource) throws XMPPErrorException, + protected Resourcepart bindResourceAndEstablishSession(Resourcepart resource) throws XMPPErrorException, SmackException, InterruptedException { // Wait until either: @@ -602,6 +635,8 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { packetCollector = createStanzaCollectorAndSend(new StanzaIdFilter(session), session); packetCollector.nextResultOrThrow(); } + + return response.getJid().getResourcepart(); } protected void afterSuccessfulLogin(final boolean resumed) throws NotConnectedException, InterruptedException { @@ -703,6 +738,7 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { } } + // TODO: This method should be final. @Override public void sendStanza(Stanza stanza) throws NotConnectedException, InterruptedException { Objects.requireNonNull(stanza, "Stanza must not be null"); @@ -781,11 +817,61 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { callConnectionClosedListener(); } + /** + * Sends out a notification that there was an error with the connection + * and closes the connection. + * + * @param exception the exception that causes the connection close event. + */ + protected final synchronized void notifyConnectionError(Exception exception) { + if (!isConnected()) { + LOGGER.log(Level.INFO, "Connection was already disconnected when attempting to handle " + exception, + exception); + return; + } + + currentConnectionException = exception; + notifyAll(); + + for (StanzaCollector collector : collectors) { + collector.notifyConnectionError(exception); + } + // TODO: We should also notify things like the SASL authentication machinery about the 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(); + + callConnectionClosedOnErrorListener(exception); + } + + protected void instantShutdown() { + // Default implementation simply calls shutdown(), subclasses may override this. + shutdown(); + } + /** * Shuts the current connection down. */ protected abstract void shutdown(); + protected final boolean waitForClosingStreamTagFromServer() { + Exception exception; + try { + // 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; + } + if (exception != null) { + LOGGER.log(Level.INFO, "Exception while waiting for closing stream element from the server " + this, exception); + } + return exception == null; + } + @Override public void addConnectionListener(ConnectionListener connectionListener) { if (connectionListener == null) { @@ -910,20 +996,25 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { } /** - * Process all stanza listeners for sending packets. + * Process all stanza listeners for sending stanzas. *

* Compared to {@link #firePacketInterceptors(Stanza)}, the listeners will be invoked in a new thread. *

* - * @param packet the stanza to process. + * @param sendTopLevelStreamElement the top level stream element which just got send. */ + // TODO: Rename to fireElementSendingListeners(). @SuppressWarnings("javadoc") - protected void firePacketSendingListeners(final Stanza packet) { - final SmackDebugger debugger = this.debugger; + protected void firePacketSendingListeners(final TopLevelStreamElement sendTopLevelStreamElement) { if (debugger != null) { - debugger.onOutgoingStreamElement(packet); + debugger.onOutgoingStreamElement(sendTopLevelStreamElement); } + if (!(sendTopLevelStreamElement instanceof Stanza)) { + return; + } + Stanza packet = (Stanza) sendTopLevelStreamElement; + final List listenersToNotify = new LinkedList<>(); synchronized (sendListeners) { for (ListenerWrapper listenerWrapper : sendListeners.values()) { @@ -1037,6 +1128,49 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { this.unknownIqRequestReplyMode = Objects.requireNonNull(unknownIqRequestReplyMode, "Mode must not be null"); } + protected final NonzaCallback.Builder buildNonzaCallback() { + return new NonzaCallback.Builder(this); + } + + protected SN sendAndWaitForResponse(Nonza nonza, Class successNonzaClass, + Class failedNonzaClass) + throws NoResponseException, NotConnectedException, InterruptedException, FailedNonzaException { + NonzaCallback.Builder builder = buildNonzaCallback(); + SN successNonza = NonzaCallback.sendAndWaitForResponse(builder, nonza, successNonzaClass, failedNonzaClass); + return successNonza; + } + + protected final void parseAndProcessNonza(XmlPullParser parser) throws SmackException { + final String element = parser.getName(); + final String namespace = parser.getNamespace(); + final String key = XmppStringUtils.generateKey(element, namespace); + + NonzaProvider nonzaProvider = ProviderManager.getNonzaProvider(key); + if (nonzaProvider == null) { + LOGGER.severe("Unknown nonza: " + key); + return; + } + + NonzaCallback nonzaCallback; + synchronized (nonzaCallbacks) { + nonzaCallback = nonzaCallbacks.get(key); + } + if (nonzaCallback == null) { + LOGGER.info("No nonza callback for " + key); + return; + } + + Nonza nonza; + try { + nonza = nonzaProvider.parse(parser); + } + catch (Exception e) { + throw new SmackException(e); + } + + nonzaCallback.onNonzaReceived(nonza); + } + protected void parseAndProcessStanza(XmlPullParser parser) throws Exception { ParserUtils.assertAtStartTag(parser); int parserDepth = parser.getDepth(); @@ -1130,14 +1264,18 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { // If the IQ stanza is of type "get" or "set" with no registered IQ request handler, then answer an // IQ of type 'error' with condition 'service-unavailable'. - ErrorIQ errorIQ = IQ.createErrorResponse(iq, StanzaError.getBuilder( + final ErrorIQ errorIQ = IQ.createErrorResponse(iq, StanzaError.getBuilder( replyCondition)); - try { - sendStanza(errorIQ); - } - catch (InterruptedException | NotConnectedException e) { - LOGGER.log(Level.WARNING, "Exception while sending error IQ to unkown IQ request", e); - } + // Use async sendStanza() here, since if sendStanza() would block, then some connections, e.g. + // XmppNioTcpConnection, would deadlock, as this operation is performed in the same thread that is + asyncGo(() -> { + try { + sendStanza(errorIQ); + } + catch (InterruptedException | NotConnectedException e) { + LOGGER.log(Level.WARNING, "Exception while sending error IQ to unkown IQ request", e); + } + }); } else { Executor executorService = null; switch (iqRequestHandler.getMode()) { @@ -1297,7 +1435,7 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { } } - protected void callConnectionClosedOnErrorListener(Exception e) { + private void callConnectionClosedOnErrorListener(Exception e) { boolean logWarning = true; if (e instanceof StreamErrorException) { StreamErrorException see = (StreamErrorException) e; @@ -1401,7 +1539,7 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { int eventType = parser.next(); if (eventType == XmlPullParser.START_TAG && parser.getDepth() == initialDepth + 1) { - ExtensionElement streamFeature = null; + FullyQualifiedElement streamFeature = null; String name = parser.getName(); String namespace = parser.getNamespace(); switch (name) { @@ -1435,6 +1573,10 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { break; } } + } + + protected final void parseFeaturesAndNotify(XmlPullParser parser) throws Exception { + parseFeatures(parser); if (hasFeature(Mechanisms.ELEMENT, Mechanisms.NAMESPACE)) { // Only proceed with SASL auth if TLS is disabled or if the server doesn't announce it @@ -1465,7 +1607,7 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { @SuppressWarnings("unchecked") @Override - public F getFeature(String element, String namespace) { + public F getFeature(String element, String namespace) { return (F) streamFeatures.get(XmppStringUtils.generateKey(element, namespace)); } @@ -1474,7 +1616,7 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { return getFeature(element, namespace) != null; } - protected void addStreamFeature(ExtensionElement feature) { + protected void addStreamFeature(FullyQualifiedElement feature) { String key = XmppStringUtils.generateKey(feature.getElementName(), feature.getNamespace()); streamFeatures.put(key, feature); } @@ -1659,7 +1801,155 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { CACHED_EXECUTOR_SERVICE.execute(runnable); } - protected static ScheduledFuture schedule(Runnable runnable, long delay, TimeUnit unit) { - return SCHEDULED_EXECUTOR_SERVICE.schedule(runnable, delay, unit); + protected static ScheduledAction schedule(Runnable runnable, long delay, TimeUnit unit) { + return SMACK_REACTOR.schedule(runnable, delay, unit); + } + + protected void onStreamOpen(XmlPullParser parser) { + // We found an opening stream. + if ("jabber:client".equals(parser.getNamespace(null))) { + streamId = parser.getAttributeValue("", "id"); + String reportedServerDomain = parser.getAttributeValue("", "from"); + assert (config.getXMPPServiceDomain().equals(reportedServerDomain)); + } + } + + protected void sendStreamOpen() throws NotConnectedException, InterruptedException { + // If possible, provide the receiving entity of the stream open tag, i.e. the server, as much information as + // possible. The 'to' attribute is *always* available. The 'from' attribute if set by the user and no external + // mechanism is used to determine the local entity (user). And the 'id' attribute is available after the first + // response from the server (see e.g. RFC 6120 ยง 9.1.1 Step 2.) + CharSequence to = getXMPPServiceDomain(); + CharSequence from = null; + CharSequence localpart = config.getUsername(); + if (localpart != null) { + from = XmppStringUtils.completeJidFrom(localpart, to); + } + String id = getStreamId(); + sendNonza(new StreamOpen(to, from, id)); + } + + public static final class SmackTlsContext { + public final SSLContext sslContext; + public final SmackDaneVerifier daneVerifier; + + private SmackTlsContext(SSLContext sslContext, SmackDaneVerifier daneVerifier) { + assert sslContext != null; + this.sslContext = sslContext; + this.daneVerifier = daneVerifier; + } + } + + protected final SmackTlsContext getSmackTlsContext() throws KeyManagementException, NoSuchAlgorithmException, + CertificateException, IOException, UnrecoverableKeyException, KeyStoreException, NoSuchProviderException { + SmackDaneVerifier daneVerifier = null; + + if (config.getDnssecMode() == DnssecMode.needsDnssecAndDane) { + SmackDaneProvider daneProvider = DNSUtil.getDaneProvider(); + if (daneProvider == null) { + throw new UnsupportedOperationException("DANE enabled but no SmackDaneProvider configured"); + } + daneVerifier = daneProvider.newInstance(); + if (daneVerifier == null) { + throw new IllegalStateException("DANE requested but DANE provider did not return a DANE verifier"); + } + } + + SSLContext context = this.config.getCustomSSLContext(); + KeyStore ks = null; + PasswordCallback pcb = null; + + if (context == null) { + final String keyStoreType = config.getKeystoreType(); + final CallbackHandler callbackHandler = config.getCallbackHandler(); + final String keystorePath = config.getKeystorePath(); + if ("PKCS11".equals(keyStoreType)) { + try { + Constructor c = Class.forName("sun.security.pkcs11.SunPKCS11").getConstructor(InputStream.class); + String pkcs11Config = "name = SmartCard\nlibrary = " + config.getPKCS11Library(); + ByteArrayInputStream config = new ByteArrayInputStream(pkcs11Config.getBytes(StringUtils.UTF8)); + Provider p = (Provider) c.newInstance(config); + Security.addProvider(p); + ks = KeyStore.getInstance("PKCS11",p); + pcb = new PasswordCallback("PKCS11 Password: ",false); + callbackHandler.handle(new Callback[] {pcb}); + ks.load(null,pcb.getPassword()); + } + catch (Exception e) { + LOGGER.log(Level.WARNING, "Exception", e); + ks = null; + } + } + else if ("Apple".equals(keyStoreType)) { + ks = KeyStore.getInstance("KeychainStore","Apple"); + ks.load(null,null); + // pcb = new PasswordCallback("Apple Keychain",false); + // pcb.setPassword(null); + } + else if (keyStoreType != null) { + ks = KeyStore.getInstance(keyStoreType); + if (callbackHandler != null && StringUtils.isNotEmpty(keystorePath)) { + try { + pcb = new PasswordCallback("Keystore Password: ", false); + callbackHandler.handle(new Callback[] { pcb }); + ks.load(new FileInputStream(keystorePath), pcb.getPassword()); + } + catch (Exception e) { + LOGGER.log(Level.WARNING, "Exception", e); + ks = null; + } + } else { + ks.load(null, null); + } + } + + KeyManager[] kms = null; + + if (ks != null) { + String keyManagerFactoryAlgorithm = KeyManagerFactory.getDefaultAlgorithm(); + KeyManagerFactory kmf = null; + try { + kmf = KeyManagerFactory.getInstance(keyManagerFactoryAlgorithm); + } + catch (NoSuchAlgorithmException e) { + LOGGER.log(Level.FINE, "Could get the default KeyManagerFactory for the '" + + keyManagerFactoryAlgorithm + "' algorithm", e); + } + if (kmf != null) { + try { + if (pcb == null) { + kmf.init(ks, null); + } + else { + kmf.init(ks, pcb.getPassword()); + pcb.clearPassword(); + } + kms = kmf.getKeyManagers(); + } + catch (NullPointerException npe) { + LOGGER.log(Level.WARNING, "NullPointerException", npe); + } + } + } + + // If the user didn't specify a SSLContext, use the default one + context = SSLContext.getInstance("TLS"); + + final SecureRandom secureRandom = new java.security.SecureRandom(); + X509TrustManager customTrustManager = config.getCustomX509TrustManager(); + + if (daneVerifier != null) { + // User requested DANE verification. + daneVerifier.init(context, kms, customTrustManager, secureRandom); + } else { + TrustManager[] customTrustManagers = null; + if (customTrustManager != null) { + customTrustManagers = new TrustManager[] { customTrustManager }; + } + context.init(kms, customTrustManagers, secureRandom); + } + } + + return new SmackTlsContext(context, daneVerifier); } } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/AbstractXmppNioConnection.java b/smack-core/src/main/java/org/jivesoftware/smack/AbstractXmppNioConnection.java new file mode 100644 index 000000000..b54f77228 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/AbstractXmppNioConnection.java @@ -0,0 +1,54 @@ +/** + * + * Copyright 2018 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack; + +import java.nio.channels.ClosedChannelException; +import java.nio.channels.SelectableChannel; +import java.nio.channels.SelectionKey; + +import org.jivesoftware.smack.SmackReactor.ChannelSelectedCallback; +import org.jivesoftware.smack.fsm.AbstractXmppStateMachineConnection; +import org.jivesoftware.smack.fsm.StateDescriptor; +import org.jivesoftware.smack.fsm.StateDescriptorGraph.GraphVertex; + +public abstract class AbstractXmppNioConnection extends AbstractXmppStateMachineConnection { + + protected AbstractXmppNioConnection(ConnectionConfiguration configuration, GraphVertex initialStateDescriptorVertex) { + super(configuration, initialStateDescriptorVertex); + } + + protected SelectionKey registerWithSelector(SelectableChannel channel, int ops, ChannelSelectedCallback callback) + throws ClosedChannelException { + return SMACK_REACTOR.registerWithSelector(channel, ops, callback); + } + + /** + * Set the interest Ops of a SelectionKey. Since Java's NIO interestOps(int) can block at any time, we use a queue + * to perform the actual operation in the reactor where we can perform this operation non-blocking. + * + * @param selectionKey + * @param interestOps + */ + protected void setInterestOps(SelectionKey selectionKey, int interestOps) { + SMACK_REACTOR.setInterestOps(selectionKey, interestOps); + } + + @Override + protected void finalize() { + disconnect(); + } +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/GenericElementListener.java b/smack-core/src/main/java/org/jivesoftware/smack/GenericElementListener.java new file mode 100644 index 000000000..959c21873 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/GenericElementListener.java @@ -0,0 +1,36 @@ +/** + * + * Copyright 2018 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack; + +import org.jivesoftware.smack.packet.Element; + +public abstract class GenericElementListener { + + private final Class elementClass; + + public GenericElementListener(Class elementClass) { + this.elementClass = elementClass; + } + + public abstract void process(E element); + + public final void processElement(Element element) { + E concreteEleement = elementClass.cast(element); + process(concreteEleement); + } + +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/Manager.java b/smack-core/src/main/java/org/jivesoftware/smack/Manager.java index b3c648dd3..d3c4beec1 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/Manager.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/Manager.java @@ -17,7 +17,6 @@ package org.jivesoftware.smack; import java.lang.ref.WeakReference; -import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import org.jivesoftware.smack.SmackException.NotLoggedInException; @@ -54,7 +53,7 @@ public abstract class Manager { return connection; } - protected static final ScheduledFuture schedule(Runnable runnable, long delay, TimeUnit unit) { - return AbstractXMPPConnection.SCHEDULED_EXECUTOR_SERVICE.schedule(runnable, delay, unit); + protected static final ScheduledAction schedule(Runnable runnable, long delay, TimeUnit unit) { + return AbstractXMPPConnection.SMACK_REACTOR.schedule(runnable, delay, unit); } } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/NonzaCallback.java b/smack-core/src/main/java/org/jivesoftware/smack/NonzaCallback.java new file mode 100644 index 000000000..acc776533 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/NonzaCallback.java @@ -0,0 +1,176 @@ +/** + * + * Copyright 2018 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack; + +import java.util.HashMap; +import java.util.Map; + +import org.jivesoftware.smack.SmackException.NoResponseException; +import org.jivesoftware.smack.SmackException.NotConnectedException; +import org.jivesoftware.smack.XMPPException.FailedNonzaException; +import org.jivesoftware.smack.packet.Nonza; +import org.jivesoftware.smack.util.XmppElementUtil; + +import org.jxmpp.util.XmppStringUtils; + +public class NonzaCallback { + + protected final AbstractXMPPConnection connection; + protected final Map> filterAndListeners; + + private NonzaCallback(Builder builder) { + this.connection = builder.connection; + this.filterAndListeners = builder.filterAndListeners; + install(); + } + + void onNonzaReceived(Nonza nonza) { + String key = XmppStringUtils.generateKey(nonza.getElementName(), nonza.getNamespace()); + GenericElementListener nonzaListener = filterAndListeners.get(key); + + nonzaListener.processElement(nonza); + } + + public void cancel() { + synchronized (connection.nonzaCallbacks) { + for (Map.Entry> entry : filterAndListeners.entrySet()) { + String filterKey = entry.getKey(); + NonzaCallback installedCallback = connection.nonzaCallbacks.get(filterKey); + if (equals(installedCallback)) { + connection.nonzaCallbacks.remove(filterKey); + } + } + } + } + + protected void install() { + if (filterAndListeners.isEmpty()) { + return; + } + + synchronized (connection.nonzaCallbacks) { + for (String key : filterAndListeners.keySet()) { + connection.nonzaCallbacks.put(key, this); + } + } + } + + private static final class NonzaResponseCallback extends NonzaCallback { + + private SN successNonza; + private FN failedNonza; + + private NonzaResponseCallback(Class successNonzaClass, Class failedNonzaClass, + Builder builder) { + super(builder); + + final String successNonzaKey = XmppElementUtil.getKeyFor(successNonzaClass); + final String failedNonzaKey = XmppElementUtil.getKeyFor(failedNonzaClass); + + final GenericElementListener successListener = new GenericElementListener(successNonzaClass) { + @Override + public void process(SN successNonza) { + NonzaResponseCallback.this.successNonza = successNonza; + notifyResponse(); + } + }; + + final GenericElementListener failedListener = new GenericElementListener(failedNonzaClass) { + @Override + public void process(FN failedNonza) { + NonzaResponseCallback.this.failedNonza = failedNonza; + notifyResponse(); + } + }; + + filterAndListeners.put(successNonzaKey, successListener); + filterAndListeners.put(failedNonzaKey, failedListener); + + install(); + } + + private void notifyResponse() { + synchronized (this) { + notifyAll(); + } + } + + private boolean hasReceivedSuccessOrFailedNonza() { + return successNonza != null || failedNonza != null; + } + + private SN waitForResponse() throws NoResponseException, InterruptedException, FailedNonzaException { + final long deadline = System.currentTimeMillis() + connection.getReplyTimeout(); + synchronized (this) { + while (!hasReceivedSuccessOrFailedNonza()) { + final long now = System.currentTimeMillis(); + if (now >= deadline) break; + wait(deadline - now); + } + } + + if (!hasReceivedSuccessOrFailedNonza()) { + throw NoResponseException.newWith(connection, "Nonza Listener"); + } + + if (failedNonza != null) { + throw new XMPPException.FailedNonzaException(failedNonza); + } + + assert successNonza != null; + return successNonza; + } + } + + public static final class Builder { + private final AbstractXMPPConnection connection; + + private Map> filterAndListeners = new HashMap<>(); + + Builder(AbstractXMPPConnection connection) { + this.connection = connection; + } + + public Builder listenFor(Class nonza, GenericElementListener nonzaListener) { + String key = XmppElementUtil.getKeyFor(nonza); + filterAndListeners.put(key, nonzaListener); + return this; + } + + public NonzaCallback install() { + return new NonzaCallback(this); + } + } + + static SN sendAndWaitForResponse(NonzaCallback.Builder builder, Nonza nonza, Class successNonzaClass, + Class failedNonzaClass) + throws NoResponseException, NotConnectedException, InterruptedException, FailedNonzaException { + NonzaResponseCallback nonzaCallback = new NonzaResponseCallback<>(successNonzaClass, + failedNonzaClass, builder); + + SN successNonza; + try { + nonzaCallback.connection.sendNonza(nonza); + successNonza = nonzaCallback.waitForResponse(); + } + finally { + nonzaCallback.cancel(); + } + + return successNonza; + } +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/SASLAuthentication.java b/smack-core/src/main/java/org/jivesoftware/smack/SASLAuthentication.java index 00314a3ca..3a8ebc5ae 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/SASLAuthentication.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/SASLAuthentication.java @@ -180,13 +180,14 @@ public final class SASLAuthentication { * @param password the password to send to the server. * @param authzid the authorization identifier (typically null). * @param sslSession the optional SSL/TLS session (if one was established) + * @return the used SASLMechanism. * @throws XMPPErrorException * @throws SASLErrorException * @throws IOException * @throws SmackException * @throws InterruptedException */ - public void authenticate(String username, String password, EntityBareJid authzid, SSLSession sslSession) + public SASLMechanism authenticate(String username, String password, EntityBareJid authzid, SSLSession sslSession) throws XMPPErrorException, SASLErrorException, IOException, SmackException, InterruptedException { currentMechanism = selectMechanism(authzid); @@ -223,6 +224,8 @@ public final class SASLAuthentication { if (!authenticationSuccessful) { throw NoResponseException.newWith(connection, "successful SASL authentication"); } + + return currentMechanism; } /** diff --git a/smack-core/src/main/java/org/jivesoftware/smack/ScheduledAction.java b/smack-core/src/main/java/org/jivesoftware/smack/ScheduledAction.java new file mode 100644 index 000000000..5b5f0b238 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/ScheduledAction.java @@ -0,0 +1,66 @@ +/** + * + * Copyright 2018 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack; + +import java.util.Date; +import java.util.concurrent.Delayed; +import java.util.concurrent.TimeUnit; + +public class ScheduledAction implements Delayed { + + final Runnable action; + final Date releaseTime; + final SmackReactor smackReactor; + + ScheduledAction(Runnable action, Date releaseTime, SmackReactor smackReactor) { + this.action = action; + this.releaseTime = releaseTime; + this.smackReactor = smackReactor; + } + + public void cancel() { + smackReactor.cancel(this); + } + + public boolean isDue() { + Date now = new Date(); + return now.after(releaseTime); + } + + public long getTimeToDueMillis() { + long now = System.currentTimeMillis(); + return releaseTime.getTime() - now; + } + + @Override + public int compareTo(Delayed otherDelayed) { + if (this == otherDelayed) { + return 0; + } + + long thisDelay = getDelay(TimeUnit.MILLISECONDS); + long otherDelay = otherDelayed.getDelay(TimeUnit.MILLISECONDS); + + return Long.compare(thisDelay, otherDelay); + } + + @Override + public long getDelay(TimeUnit unit) { + long delayInMillis = getTimeToDueMillis(); + return unit.convert(delayInMillis, TimeUnit.MILLISECONDS); + } +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/SmackException.java b/smack-core/src/main/java/org/jivesoftware/smack/SmackException.java index 74d391539..5081629ac 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/SmackException.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/SmackException.java @@ -94,12 +94,19 @@ public class SmackException extends Exception { } public static NoResponseException newWith(XMPPConnection connection, - StanzaCollector collector) { - return newWith(connection, collector.getStanzaFilter()); + StanzaCollector collector, boolean stanzaCollectorCancelled) { + return newWith(connection, collector.getStanzaFilter(), stanzaCollectorCancelled); } public static NoResponseException newWith(XMPPConnection connection, StanzaFilter filter) { + return newWith(connection, filter, false); + } + + public static NoResponseException newWith(XMPPConnection connection, StanzaFilter filter, boolean stanzaCollectorCancelled) { final StringBuilder sb = getWaitingFor(connection); + if (stanzaCollectorCancelled) { + sb.append(" StanzaCollector has been cancelled."); + } sb.append(" Waited for response using: "); if (filter != null) { sb.append(filter.toString()); @@ -182,6 +189,12 @@ public class SmackException extends Exception { super("The connection " + connection + " is no longer connected while waiting for response with " + stanzaFilter); } + + public NotConnectedException(XMPPConnection connection, StanzaFilter stanzaFilter, + Exception connectionException) { + super("The connection " + connection + " is no longer connected while waiting for response with " + + stanzaFilter + " because of " + connectionException, connectionException); + } } public static class IllegalStateChangeException extends SmackException { @@ -283,6 +296,15 @@ public 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 { /** diff --git a/smack-core/src/main/java/org/jivesoftware/smack/SmackInitialization.java b/smack-core/src/main/java/org/jivesoftware/smack/SmackInitialization.java index f6807512b..701004696 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/SmackInitialization.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/SmackInitialization.java @@ -25,13 +25,19 @@ import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; +import org.jivesoftware.smack.compress.provider.CompressedProvider; +import org.jivesoftware.smack.compress.provider.FailureProvider; import org.jivesoftware.smack.compression.Java7ZlibInputOutputStream; +import org.jivesoftware.smack.compression.XmppCompressionManager; +import org.jivesoftware.smack.compression.zlib.ZlibXmppCompressionFactory; import org.jivesoftware.smack.initializer.SmackInitializer; import org.jivesoftware.smack.packet.Bind; import org.jivesoftware.smack.packet.Message.Body; import org.jivesoftware.smack.provider.BindIQProvider; import org.jivesoftware.smack.provider.BodyElementProvider; import org.jivesoftware.smack.provider.ProviderManager; +import org.jivesoftware.smack.provider.TlsFailureProvider; +import org.jivesoftware.smack.provider.TlsProceedProvider; import org.jivesoftware.smack.sasl.core.SASLAnonymous; import org.jivesoftware.smack.sasl.core.SASLXOauth2Mechanism; import org.jivesoftware.smack.sasl.core.SCRAMSHA1Mechanism; @@ -97,6 +103,8 @@ public final class SmackInitialization { // Add the Java7 compression handler first, since it's preferred SmackConfiguration.compressionHandlers.add(new Java7ZlibInputOutputStream()); + XmppCompressionManager.registerXmppCompressionFactory(ZlibXmppCompressionFactory.INSTANCE); + // Use try block since we may not have permission to get a system // property (for example, when an applet). try { @@ -118,6 +126,11 @@ public final class SmackInitialization { ProviderManager.addIQProvider(Bind.ELEMENT, Bind.NAMESPACE, new BindIQProvider()); ProviderManager.addExtensionProvider(Body.ELEMENT, Body.NAMESPACE, new BodyElementProvider()); + ProviderManager.addNonzaProvider(TlsProceedProvider.INSTANCE); + ProviderManager.addNonzaProvider(TlsFailureProvider.INSTANCE); + ProviderManager.addNonzaProvider(CompressedProvider.INSTANCE); + ProviderManager.addNonzaProvider(FailureProvider.INSTANCE); + SmackConfiguration.smackInitialized = true; } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/SmackReactor.java b/smack-core/src/main/java/org/jivesoftware/smack/SmackReactor.java new file mode 100644 index 000000000..ffc622633 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/SmackReactor.java @@ -0,0 +1,440 @@ +/** + * + * Copyright 2018-2019 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.io.IOException; +import java.lang.ref.WeakReference; +import java.nio.channels.CancelledKeyException; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.SelectableChannel; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.DelayQueue; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * The SmackReactor for non-blocking I/O. + *

+ * Highlights include: + *

    + *
  • Multiple reactor threads
  • + *
  • Scheduled actions
  • + *
+ * + *
+ *
+ *           ) ) )
+ *        ( ( (
+ *      ) ) )
+ *   (~~~~~~~~~)
+ *    | Smack |
+ *    |Reactor|
+ *    I      _._
+ *    I    /'   `\
+ *    I   |       |
+ *    f   |   |~~~~~~~~~~~~~~|
+ *  .'    |   | #   #   #  # |
+ * '______|___|___________###|
+ * 
+ */ +public class SmackReactor { + + private static final Logger LOGGER = Logger.getLogger(SmackReactor.class.getName()); + + private static final int DEFAULT_REACTOR_THREAD_COUNT = 2; + + private static final int PENDING_SET_INTEREST_OPS_MAX_BATCH_SIZE = 1024; + + private static SmackReactor INSTANCE; + + static synchronized SmackReactor getInstance() { + if (INSTANCE == null) { + INSTANCE = new SmackReactor("DefaultReactor"); + } + return INSTANCE; + } + + private final Selector selector; + private final String reactorName; + + private final List reactorThreads = Collections.synchronizedList(new ArrayList<>()); + + private final DelayQueue scheduledActions = new DelayQueue<>(); + + private final Lock registrationLock = new ReentrantLock(); + + /** + * The semaphore protecting the handling of the actions. Note that it is + * initialized with -1, which basically means that one thread will always do I/O using + * select(). + */ + private final Semaphore actionsSemaphore = new Semaphore(-1, false); + + private final Queue pendingSelectionKeys = new ConcurrentLinkedQueue<>(); + + private final Queue pendingSetInterestOps = new ConcurrentLinkedQueue<>(); + + SmackReactor(String reactorName) { + this.reactorName = reactorName; + + try { + selector = Selector.open(); + } + catch (IOException e) { + throw new IllegalStateException(e); + } + + setReactorThreadCount(DEFAULT_REACTOR_THREAD_COUNT); + } + + SelectionKey registerWithSelector(SelectableChannel channel, int ops, ChannelSelectedCallback callback) + throws ClosedChannelException { + SelectionKeyAttachment selectionKeyAttachment = new SelectionKeyAttachment(callback); + + registrationLock.lock(); + try { + selector.wakeup(); + return channel.register(selector, ops, selectionKeyAttachment); + } finally { + registrationLock.unlock(); + } + } + + void setInterestOps(SelectionKey selectionKey, int interestOps) { + SetInterestOps setInterestOps = new SetInterestOps(selectionKey, interestOps); + pendingSetInterestOps.add(setInterestOps); + selector.wakeup(); + } + + private static final class SetInterestOps { + private final SelectionKey selectionKey; + private final int interestOps; + + private SetInterestOps(SelectionKey selectionKey, int interestOps) { + this.selectionKey = selectionKey; + this.interestOps = interestOps; + } + } + + ScheduledAction schedule(Runnable runnable, long delay, TimeUnit unit) { + long releaseTimeEpoch = System.currentTimeMillis() + unit.toMillis(delay); + Date releaseTimeDate = new Date(releaseTimeEpoch); + ScheduledAction scheduledAction = new ScheduledAction(runnable, releaseTimeDate, this); + synchronized (scheduledActions) { + scheduledActions.add(scheduledAction); + } + return scheduledAction; + } + + boolean cancel(ScheduledAction scheduledAction) { + return scheduledActions.remove(scheduledAction); + } + + private class Reactor extends Thread { + + private volatile long shutdownRequestTimestamp = -1; + + @Override + public void run() { + try { + reactorLoop(); + } finally { + if (shutdownRequestTimestamp > 0) { + long shutDownDelay = System.currentTimeMillis() - shutdownRequestTimestamp; + LOGGER.info(this + " shut down after " + shutDownDelay + "ms"); + } else { + boolean contained = reactorThreads.remove(this); + assert (contained); + } + } + } + + private void reactorLoop() { + // Loop until reactor shutdown was requested. + while (shutdownRequestTimestamp < 0) { + handleScheduledActionsOrPerformSelect(); + + handlePendingSelectionKeys(); + } + } + + @SuppressWarnings("LockNotBeforeTry") + private void handleScheduledActionsOrPerformSelect() { + ScheduledAction dueScheduledAction = null; + + boolean permitToHandleScheduledActions = actionsSemaphore.tryAcquire(); + if (permitToHandleScheduledActions) { + try { + dueScheduledAction = scheduledActions.poll(); + } finally { + actionsSemaphore.release(); + } + } + + if (dueScheduledAction != null) { + dueScheduledAction.action.run(); + return; + } + + ScheduledAction nextScheduledAction = scheduledActions.peek(); + + long selectWait; + if (nextScheduledAction == null) { + // There is no next scheduled action, wait indefinitely in select(). + selectWait = 0; + } else { + selectWait = nextScheduledAction.getTimeToDueMillis(); + } + + if (selectWait < 0) { + // A scheduled action was just released and become ready to execute. + return; + } + + int newSelectedKeysCount = 0; + List selectedKeys; + synchronized (selector) { + // Before we call select, we handle the pending the interest Ops. This will not block since no other + // thread is currently in select() at this time. + // Note: This was put deliberately before the registration lock. It may cause more synchronization but + // allows for more parallelism. + // Hopefully that assumption is right. + int myHandledPendingSetInterestOps = 0; + for (SetInterestOps setInterestOps; (setInterestOps = pendingSetInterestOps.poll()) != null;) { + setInterestOpsCancelledKeySafe(setInterestOps.selectionKey, setInterestOps.interestOps); + + if (myHandledPendingSetInterestOps++ >= PENDING_SET_INTEREST_OPS_MAX_BATCH_SIZE) { + // This thread has handled enough "set pending interest ops" requests. Wakeup another one to + // handle the remaining (if any). + selector.wakeup(); + break; + } + } + + // Ensure that a wakeup() in registerWithSelector() gives the corresponding + // register() in the same method the chance to actually register the channel. In + // other words: This construct ensures that there is never another select() + // between a corresponding wakeup() and register() calls. + // See also https://stackoverflow.com/a/1112809/194894 + registrationLock.lock(); + registrationLock.unlock(); + + try { + newSelectedKeysCount = selector.select(selectWait); + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "IOException while using select()", e); + return; + } + + if (newSelectedKeysCount == 0) { + return; + } + + // Copy the selected-key set over to selectedKeys, remove the keys from the + // selected key set and loose interest of the key OPs for the time being. + // Note that we perform this operation in two steps in order to maximize the + // timespan setRacing() is set. + Set selectedKeySet = selector.selectedKeys(); + for (SelectionKey selectionKey : selectedKeySet) { + SelectionKeyAttachment selectionKeyAttachment = (SelectionKeyAttachment) selectionKey.attachment(); + selectionKeyAttachment.setRacing(); + } + for (SelectionKey selectionKey : selectedKeySet) { + setInterestOpsCancelledKeySafe(selectionKey, 0); + } + + selectedKeys = new ArrayList<>(selectedKeySet.size()); + selectedKeys.addAll(selectedKeySet); + selectedKeySet.clear(); + } + + int selectedKeysCount = selectedKeys.size(); + int currentReactorThreadCount = reactorThreads.size(); + int myKeyCount; + if (selectedKeysCount > currentReactorThreadCount) { + myKeyCount = selectedKeysCount / currentReactorThreadCount; + } else { + myKeyCount = selectedKeysCount; + } + + final Level reactorSelectStatsLogLevel = Level.FINE; + if (LOGGER.isLoggable(reactorSelectStatsLogLevel)) { + LOGGER.log(reactorSelectStatsLogLevel, + "New selected key count: " + newSelectedKeysCount + + ". Total selected key count " + selectedKeysCount + + ". My key count: " + myKeyCount + + ". Current reactor thread count: " + currentReactorThreadCount); + } + + Collection mySelectedKeys = new ArrayList<>(myKeyCount); + Iterator it = selectedKeys.iterator(); + for (int i = 0; i < myKeyCount; i++) { + SelectionKey selectionKey = it.next(); + mySelectedKeys.add(selectionKey); + } + while (it.hasNext()) { + // Drain to pendingSelectionKeys. + SelectionKey selectionKey = it.next(); + pendingSelectionKeys.add(selectionKey); + } + + if (selectedKeysCount - myKeyCount > 0) { + // There where pending selection keys: Wakeup another reactor thread to handle them. + selector.wakeup(); + } + + handleSelectedKeys(mySelectedKeys); + } + + private void handlePendingSelectionKeys() { + final int pendingSelectionKeysSize = pendingSelectionKeys.size(); + if (pendingSelectionKeysSize == 0) { + return; + } + + int currentReactorThreadCount = reactorThreads.size(); + int myKeyCount = pendingSelectionKeysSize / currentReactorThreadCount; + Collection selectedKeys = new ArrayList<>(myKeyCount); + for (int i = 0; i < myKeyCount; i++) { + SelectionKey selectionKey = pendingSelectionKeys.poll(); + if (selectionKey == null) { + // We lost a race and can abort here since the pendingSelectionKeys queue is empty. + break; + } + selectedKeys.add(selectionKey); + } + + if (!pendingSelectionKeys.isEmpty()) { + // There are more pending selection keys, wakeup a thread blocked in select() to handle them. + selector.wakeup(); + } + + handleSelectedKeys(selectedKeys); + } + + private void setInterestOpsCancelledKeySafe(SelectionKey selectionKey, int interestOps) { + try { + selectionKey.interestOps(interestOps); + } + catch (CancelledKeyException e) { + final Level keyCancelledLogLevel = Level.FINER; + if (LOGGER.isLoggable(keyCancelledLogLevel)) { + LOGGER.log(keyCancelledLogLevel, "Key '" + selectionKey + "' has been cancelled", e); + } + } + } + + void requestShutdown() { + shutdownRequestTimestamp = System.currentTimeMillis(); + } + } + + private static void handleSelectedKeys(Collection selectedKeys) { + for (SelectionKey selectionKey : selectedKeys) { + SelectableChannel channel = selectionKey.channel(); + SelectionKeyAttachment selectionKeyAttachment = (SelectionKeyAttachment) selectionKey.attachment(); + ChannelSelectedCallback channelSelectedCallback = selectionKeyAttachment.weaeklyReferencedChannelSelectedCallback.get(); + if (channelSelectedCallback != null) { + channelSelectedCallback.onChannelSelected(channel, selectionKey); + } + else { + selectionKey.cancel(); + } + } + } + + public interface ChannelSelectedCallback { + void onChannelSelected(SelectableChannel channel, SelectionKey selectionKey); + } + + public void setReactorThreadCount(int reactorThreadCount) { + if (reactorThreadCount < 2) { + throw new IllegalArgumentException("Must have at least two reactor threads, but you requested " + reactorThreadCount); + } + + synchronized (reactorThreads) { + int deltaThreads = reactorThreadCount - reactorThreads.size(); + if (deltaThreads > 0) { + // Start new reactor thread. Note that we start the threads before we increase the permits of the + // actionsSemaphore. + for (int i = 0; i < deltaThreads; i++) { + Reactor reactor = new Reactor(); + reactor.setDaemon(true); + reactor.setName("Smack " + reactorName + " Thread #" + i); + reactorThreads.add(reactor); + reactor.start(); + } + + actionsSemaphore.release(deltaThreads); + } else { + // Stop existing reactor threads. First we change the sign of deltaThreads, then we decrease the permits + // of the actionsSemaphore *before* we signal the selected reactor threads that they should shut down. + deltaThreads -= deltaThreads; + + for (int i = deltaThreads - 1; i > 0; i--) { + // Note that this could potentially block forever, starving on the unfair semaphore. + actionsSemaphore.acquireUninterruptibly(); + } + + for (int i = deltaThreads - 1; i > 0; i--) { + Reactor reactor = reactorThreads.remove(i); + reactor.requestShutdown(); + } + + selector.wakeup(); + } + } + } + + public static final class SelectionKeyAttachment { + private final WeakReference weaeklyReferencedChannelSelectedCallback; + private final AtomicBoolean reactorThreadRacing = new AtomicBoolean(); + + private SelectionKeyAttachment(ChannelSelectedCallback channelSelectedCallback) { + this.weaeklyReferencedChannelSelectedCallback = new WeakReference<>(channelSelectedCallback); + } + + private void setRacing() { + // We use lazySet here since it is sufficient if the value does not become visible immediately. + reactorThreadRacing.lazySet(true); + } + + public void resetReactorThreadRacing() { + reactorThreadRacing.set(false); + } + + public boolean isReactorThreadRacing() { + return reactorThreadRacing.get(); + } + + } +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/StanzaCollector.java b/smack-core/src/main/java/org/jivesoftware/smack/StanzaCollector.java index 68110281b..3a2f9a414 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/StanzaCollector.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/StanzaCollector.java @@ -17,10 +17,9 @@ package org.jivesoftware.smack; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.TimeUnit; import org.jivesoftware.smack.SmackException.NoResponseException; import org.jivesoftware.smack.SmackException.NotConnectedException; @@ -46,7 +45,9 @@ public class StanzaCollector { private final StanzaFilter packetFilter; - private final ArrayBlockingQueue resultQueue; + private final ArrayDeque resultQueue; + + private final int maxQueueSize; /** * The stanza collector which timeout for the next result will get reset once this collector collects a stanza. @@ -57,7 +58,9 @@ public class StanzaCollector { private final Stanza request; - private volatile boolean cancelled = false; + private volatile boolean cancelled; + + private Exception connectionException; /** * Creates a new stanza collector. If the stanza filter is null, then @@ -69,7 +72,8 @@ public class StanzaCollector { protected StanzaCollector(XMPPConnection connection, Configuration configuration) { this.connection = connection; this.packetFilter = configuration.packetFilter; - this.resultQueue = new ArrayBlockingQueue<>(configuration.size); + this.resultQueue = new ArrayDeque<>(configuration.size); + this.maxQueueSize = configuration.size; this.collectorToReset = configuration.collectorToReset; this.request = configuration.request; } @@ -79,12 +83,15 @@ public class StanzaCollector { * queued up. Once a stanza collector has been cancelled, it cannot be * re-enabled. Instead, a new stanza collector must be created. */ - public void cancel() { + public synchronized void cancel() { // If the packet collector has already been cancelled, do nothing. - if (!cancelled) { - cancelled = true; - connection.removeStanzaCollector(this); + if (cancelled) { + return; } + + cancelled = true; + connection.removeStanzaCollector(this); + notifyAll(); } /** @@ -119,7 +126,7 @@ public class StanzaCollector { * results. */ @SuppressWarnings("unchecked") - public

P pollResult() { + public synchronized

P pollResult() { return (P) resultQueue.poll(); } @@ -152,13 +159,20 @@ public class StanzaCollector { * @throws InterruptedException */ @SuppressWarnings("unchecked") - public

P nextResultBlockForever() throws InterruptedException { + // TODO: Consider removing this method as it is hardly ever useful. + public synchronized

P nextResultBlockForever() throws InterruptedException { throwIfCancelled(); - P res = null; - while (res == null) { - res = (P) resultQueue.take(); + + while (true) { + P res = (P) resultQueue.poll(); + if (res != null) { + return res; + } + if (cancelled) { + return null; + } + wait(); } - return res; } /** @@ -176,13 +190,13 @@ public class StanzaCollector { private volatile long waitStart; /** - * Returns the next available packet. The method call will block (not return) - * until a stanza is available or the timeout has elapsed. If the - * timeout elapses without a result, null will be returned. + * Returns the next available stanza. The method call will block (not return) until a stanza is available or the + * timeout has elapsed or if the connection was terminated because of an error. If the timeout elapses without a + * result or if there was an connection error, null will be returned. * * @param

type of the result stanza. * @param timeout the timeout in milliseconds. - * @return the next available packet. + * @return the next available stanza or null on timeout or connection error. * @throws InterruptedException */ @SuppressWarnings("unchecked") @@ -191,14 +205,17 @@ public class StanzaCollector { P res = null; long remainingWait = timeout; waitStart = System.currentTimeMillis(); - do { - res = (P) resultQueue.poll(remainingWait, TimeUnit.MILLISECONDS); - if (res != null) { - return res; + while (remainingWait > 0 && connectionException == null && !cancelled) { + synchronized (this) { + res = (P) resultQueue.poll(); + if (res != null) { + return res; + } + wait(remainingWait); } remainingWait = timeout - (System.currentTimeMillis() - waitStart); - } while (remainingWait > 0); - return null; + } + return res; } /** @@ -263,10 +280,13 @@ public class StanzaCollector { cancel(); } if (result == null) { + if (connectionException != null) { + throw new NotConnectedException(connection, packetFilter, connectionException); + } if (!connection.isConnected()) { throw new NotConnectedException(connection, packetFilter); } - throw NoResponseException.newWith(connection, this); + throw NoResponseException.newWith(connection, this, cancelled); } XMPPErrorException.ifHasErrorThenThrow(result); @@ -289,7 +309,7 @@ public class StanzaCollector { if (collectedCache == null) { collectedCache = new ArrayList<>(getCollectedCount()); - resultQueue.drainTo(collectedCache); + collectedCache.addAll(resultQueue); } return collectedCache; @@ -301,10 +321,15 @@ public class StanzaCollector { * @return the count of collected stanzas. * @since 4.1 */ - public int getCollectedCount() { + public synchronized int getCollectedCount() { return resultQueue.size(); } + synchronized void notifyConnectionError(Exception exception) { + connectionException = exception; + notifyAll(); + } + /** * Processes a stanza to see if it meets the criteria for this stanza collector. * If so, the stanza is added to the result queue. @@ -313,12 +338,14 @@ public class StanzaCollector { */ protected void processStanza(Stanza packet) { if (packetFilter == null || packetFilter.accept(packet)) { - // CHECKSTYLE:OFF - while (!resultQueue.offer(packet)) { - // Since we know the queue is full, this poll should never actually block. - resultQueue.poll(); - } - // CHECKSTYLE:ON + synchronized (this) { + if (resultQueue.size() == maxQueueSize) { + Stanza rolledOverStanza = resultQueue.poll(); + assert rolledOverStanza != null; + } + resultQueue.add(packet); + notifyAll(); + } if (collectorToReset != null) { collectorToReset.waitStart = System.currentTimeMillis(); } @@ -403,4 +430,5 @@ public class StanzaCollector { return this; } } + } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/SynchronizationPoint.java b/smack-core/src/main/java/org/jivesoftware/smack/SynchronizationPoint.java index 9dfabfd49..4d91b67d3 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/SynchronizationPoint.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/SynchronizationPoint.java @@ -1,6 +1,6 @@ /** * - * Copyright ยฉ 2014-2015 Florian Schmaus + * Copyright ยฉ 2014-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. @@ -38,6 +38,8 @@ public class SynchronizationPoint { private State state; private E failureException; + private volatile long waitStart; + /** * Construct a new synchronization point for the given connection. * @@ -239,6 +241,10 @@ public class SynchronizationPoint { } } + 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 @@ -247,13 +253,23 @@ public class SynchronizationPoint { * @throws InterruptedException */ private void waitForConditionOrTimeout() throws InterruptedException { - long remainingWait = TimeUnit.MILLISECONDS.toNanos(connection.getReplyTimeout()); + 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; } - remainingWait = condition.awaitNanos(remainingWait); + + try { + condition.awaitNanos(remainingWait); + } catch (InterruptedException e) { + state = State.Interrupted; + throw e; + } } } @@ -286,5 +302,6 @@ public class SynchronizationPoint { NoResponse, Success, Failure, + Interrupted, } } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/XMPPConnection.java b/smack-core/src/main/java/org/jivesoftware/smack/XMPPConnection.java index 3ba0ea8a0..a871ed343 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/XMPPConnection.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/XMPPConnection.java @@ -25,6 +25,7 @@ import org.jivesoftware.smack.filter.IQReplyFilter; import org.jivesoftware.smack.filter.StanzaFilter; import org.jivesoftware.smack.iqrequest.IQRequestHandler; import org.jivesoftware.smack.packet.ExtensionElement; +import org.jivesoftware.smack.packet.FullyQualifiedElement; import org.jivesoftware.smack.packet.IQ; import org.jivesoftware.smack.packet.Nonza; import org.jivesoftware.smack.packet.Stanza; @@ -474,7 +475,7 @@ public interface XMPPConnection { * @param namespace * @return a stanza extensions of the feature or null */ - F getFeature(String element, String namespace); + F getFeature(String element, String namespace); /** * Return true if the server supports the given stream feature. @@ -565,5 +566,4 @@ public interface XMPPConnection { * @return the timestamp in milliseconds */ long getLastStanzaReceived(); - } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/XMPPException.java b/smack-core/src/main/java/org/jivesoftware/smack/XMPPException.java index ae3b6c399..9ac7518d3 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/XMPPException.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/XMPPException.java @@ -198,6 +198,10 @@ public abstract class XMPPException extends Exception { private final Nonza nonza; + public FailedNonzaException(Nonza failedNonza) { + this(failedNonza, null); + } + public FailedNonzaException(Nonza nonza, StanzaError.Condition condition) { this.condition = condition; this.nonza = nonza; diff --git a/smack-core/src/main/java/org/jivesoftware/smack/XmppInputOutputFilter.java b/smack-core/src/main/java/org/jivesoftware/smack/XmppInputOutputFilter.java new file mode 100644 index 000000000..b7b14ce05 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/XmppInputOutputFilter.java @@ -0,0 +1,76 @@ +/** + * + * Copyright 2018 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.security.cert.CertificateException; + +import org.jivesoftware.smack.SmackException.NoResponseException; + +public interface XmppInputOutputFilter { + + /** + * The {@code outputData} argument may be a direct {@link ByteBuffer}. The filter has consume the data of the buffer + * completely. + * + * This method must return a {@link OutputResult}. Use {@link OutputResult#NO_OUTPUT} if there is no output. + * + * @param outputData the data this method needs to process. + * @param isFinalDataOfElement if this is the final data of the element. + * @param destinationAddressChanged if the destination address has changed. + * @param moreDataAvailable if more data is available. + * @return a output result. + * @throws IOException in case an I/O exception occurs. + */ + OutputResult output(ByteBuffer outputData, boolean isFinalDataOfElement, boolean destinationAddressChanged, + boolean moreDataAvailable) throws IOException; + + class OutputResult { + public static final OutputResult NO_OUTPUT = new OutputResult(false, null); + + public final boolean pendingFilterData; + public final ByteBuffer filteredOutputData; + + public OutputResult(ByteBuffer filteredOutputData) { + this(false, filteredOutputData); + } + + public OutputResult(boolean pendingFilterData, ByteBuffer filteredOutputData) { + this.pendingFilterData = pendingFilterData; + this.filteredOutputData = filteredOutputData; + } + } + + /** + * The returned {@link ByteBuffer} is going to get fliped by the caller. The callee must not flip the buffer. + * @param inputData the data this methods needs to process. + * @return a {@link ByteBuffer} or {@code null} if no data could be produced. + * @throws IOException in case an I/O exception occurs. + */ + ByteBuffer input(ByteBuffer inputData) throws IOException; + + default void closeInputOutput() { + } + + default void waitUntilInputOutputClosed() throws IOException, NoResponseException, CertificateException, InterruptedException, SmackException { + } + + default Object getStats() { + return null; + } +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/compress/packet/Compress.java b/smack-core/src/main/java/org/jivesoftware/smack/compress/packet/Compress.java index bb39fb911..91d96e6de 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/compress/packet/Compress.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/compress/packet/Compress.java @@ -46,7 +46,7 @@ public class Compress implements Nonza { @Override public XmlStringBuilder toXML(String enclosingNamespace) { - XmlStringBuilder xml = new XmlStringBuilder(this); + XmlStringBuilder xml = new XmlStringBuilder(this, enclosingNamespace); xml.rightAngleBracket(); xml.element("method", method); xml.closeElement(this); 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 new file mode 100644 index 000000000..8d52752a3 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/compress/packet/Failure.java @@ -0,0 +1,89 @@ +/** + * + * Copyright 2018 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.compress.packet; + +import java.util.Objects; + +import org.jivesoftware.smack.packet.Nonza; +import org.jivesoftware.smack.packet.StanzaError; +import org.jivesoftware.smack.util.XmlStringBuilder; + +public class Failure implements Nonza { + + public static final String ELEMENT = "failure"; + public static final String NAMESPACE = Compress.NAMESPACE; + + public enum CompressFailureError { + setup_failed, + processing_failed, + unsupported_method, + ; + + private final String compressFailureError; + + CompressFailureError() { + compressFailureError = name().replace('_', '-'); + } + + @Override + public String toString() { + return compressFailureError; + } + } + + private final CompressFailureError compressFailureError; + private final StanzaError stanzaError; + + public Failure(CompressFailureError compressFailureError) { + this(compressFailureError, null); + } + + public Failure(CompressFailureError compressFailureError, StanzaError stanzaError) { + this.compressFailureError = Objects.requireNonNull(compressFailureError); + this.stanzaError = stanzaError; + } + + @Override + public String getElementName() { + return ELEMENT; + } + + @Override + public String getNamespace() { + return NAMESPACE; + } + + public CompressFailureError getCompressFailureError() { + return compressFailureError; + } + + public StanzaError getStanzaError() { + return stanzaError; + } + + @Override + public XmlStringBuilder toXML(String enclosingNamespace) { + XmlStringBuilder xml = new XmlStringBuilder(this, enclosingNamespace); + xml.rightAngleBracket(); + + xml.emptyElement(compressFailureError); + xml.optElement(stanzaError); + + xml.closeElement(this); + return xml; + } +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/compress/provider/CompressedProvider.java b/smack-core/src/main/java/org/jivesoftware/smack/compress/provider/CompressedProvider.java new file mode 100644 index 000000000..185e9ead9 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/compress/provider/CompressedProvider.java @@ -0,0 +1,36 @@ +/** + * + * Copyright 2018 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.compress.provider; + +import org.jivesoftware.smack.compress.packet.Compressed; +import org.jivesoftware.smack.provider.NonzaProvider; + +import org.xmlpull.v1.XmlPullParser; + +public final class CompressedProvider extends NonzaProvider { + + public static final CompressedProvider INSTANCE = new CompressedProvider(); + + private CompressedProvider() { + } + + @Override + public Compressed parse(XmlPullParser parser, int initialDepth) { + return Compressed.INSTANCE; + } + +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/compress/provider/FailureProvider.java b/smack-core/src/main/java/org/jivesoftware/smack/compress/provider/FailureProvider.java new file mode 100644 index 000000000..ca2916ea0 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/compress/provider/FailureProvider.java @@ -0,0 +1,81 @@ +/** + * + * Copyright 2018 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.compress.provider; + +import java.util.logging.Logger; + +import org.jivesoftware.smack.compress.packet.Failure; +import org.jivesoftware.smack.packet.StanzaError; +import org.jivesoftware.smack.packet.StreamOpen; +import org.jivesoftware.smack.provider.NonzaProvider; +import org.jivesoftware.smack.util.PacketParserUtils; + +import org.xmlpull.v1.XmlPullParser; + +public final class FailureProvider extends NonzaProvider { + + private static final Logger LOGGER = Logger.getLogger(FailureProvider.class.getName()); + + public static final FailureProvider INSTANCE = new FailureProvider(); + + private FailureProvider() { + } + + @Override + public Failure parse(XmlPullParser parser, int initialDepth) throws Exception { + Failure.CompressFailureError compressFailureError = null; + StanzaError stanzaError = null; + + outerloop: while (true) { + int eventType = parser.next(); + switch (eventType) { + case XmlPullParser.START_TAG: + String name = parser.getName(); + String namespace = parser.getNamespace(); + switch (namespace) { + case Failure.NAMESPACE: + compressFailureError = Failure.CompressFailureError.valueOf(name.replace("-", "_")); + if (compressFailureError == null) { + LOGGER.warning("Unknown element in " + Failure.NAMESPACE + ": " + name); + } + break; + case StreamOpen.CLIENT_NAMESPACE: + case StreamOpen.SERVER_NAMESPACE: + switch (name) { + case StanzaError.ERROR: + StanzaError.Builder stanzaErrorBuilder = PacketParserUtils.parseError(parser); + stanzaError = stanzaErrorBuilder.build(); + break; + default: + LOGGER.warning("Unknown element in " + namespace + ": " + name); + break; + } + break; + } + break; + case XmlPullParser.END_TAG: + if (parser.getDepth() == initialDepth) { + break outerloop; + } + break; + } + } + + return new Failure(compressFailureError, stanzaError); + } + +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/compress/provider/package-info.java b/smack-core/src/main/java/org/jivesoftware/smack/compress/provider/package-info.java new file mode 100644 index 000000000..7b31bfc23 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/compress/provider/package-info.java @@ -0,0 +1,21 @@ +/** + * + * Copyright 2018 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Providers for XMPP stream compression (XEP-138). + */ +package org.jivesoftware.smack.compress.provider; diff --git a/smack-core/src/main/java/org/jivesoftware/smack/compression/XMPPInputOutputStream.java b/smack-core/src/main/java/org/jivesoftware/smack/compression/XMPPInputOutputStream.java index c4f38816e..7272ffe1f 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/compression/XMPPInputOutputStream.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/compression/XMPPInputOutputStream.java @@ -1,6 +1,6 @@ /** * - * Copyright 2013 Florian Schmaus + * Copyright 2013-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. @@ -36,6 +36,10 @@ public abstract class XMPPInputOutputStream { XMPPInputOutputStream.flushMethod = flushMethod; } + public static FlushMethod getFlushMethod() { + return flushMethod; + } + protected final String compressionMethod; protected XMPPInputOutputStream(String compressionMethod) { diff --git a/smack-core/src/main/java/org/jivesoftware/smack/compression/XmppCompressionFactory.java b/smack-core/src/main/java/org/jivesoftware/smack/compression/XmppCompressionFactory.java new file mode 100644 index 000000000..05775c00d --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/compression/XmppCompressionFactory.java @@ -0,0 +1,47 @@ +/** + * + * Copyright 2018 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.compression; + +import org.jivesoftware.smack.ConnectionConfiguration; +import org.jivesoftware.smack.XmppInputOutputFilter; + +public abstract class XmppCompressionFactory implements Comparable { + + private final String method; + private final int priority; + + protected XmppCompressionFactory(String method, int priority) { + this.method = method; + this.priority = priority; + } + + public final String getCompressionMethod() { + return method; + } + + public final int getPriority() { + return priority; + } + + @Override + public final int compareTo(XmppCompressionFactory other) { + return Integer.compare(getPriority(), other.getPriority()); + } + + public abstract XmppInputOutputFilter fabricate(ConnectionConfiguration configuration); + +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/compression/XmppCompressionManager.java b/smack-core/src/main/java/org/jivesoftware/smack/compression/XmppCompressionManager.java new file mode 100644 index 000000000..9f8c25c61 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/compression/XmppCompressionManager.java @@ -0,0 +1,65 @@ +/** + * + * Copyright 2018 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.compression; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import org.jivesoftware.smack.compress.packet.Compress; + +public class XmppCompressionManager { + + private static final List xmppCompressionFactories = new ArrayList<>(4); + + public static XmppCompressionFactory registerXmppCompressionFactory(XmppCompressionFactory xmppCompressionFactory) { + final String method = xmppCompressionFactory.getCompressionMethod(); + XmppCompressionFactory previousFactory = null; + + synchronized (xmppCompressionFactories) { + for (Iterator it = xmppCompressionFactories.iterator(); it.hasNext(); ) { + XmppCompressionFactory factory = it.next(); + if (factory.getCompressionMethod().equals(method)) { + it.remove(); + previousFactory = factory; + break; + } + } + + xmppCompressionFactories.add(xmppCompressionFactory); + + Collections.sort(xmppCompressionFactories); + } + + return previousFactory; + } + + public static XmppCompressionFactory getBestFactory(Compress.Feature compressFeature) { + List announcedMethods = compressFeature.getMethods(); + + synchronized (xmppCompressionFactories) { + for (XmppCompressionFactory factory : xmppCompressionFactories) { + if (announcedMethods.contains(factory.getCompressionMethod())) { + return factory; + } + } + } + + return null; + } +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/compression/zlib/ZlibXmppCompressionFactory.java b/smack-core/src/main/java/org/jivesoftware/smack/compression/zlib/ZlibXmppCompressionFactory.java new file mode 100644 index 000000000..c8e3f6381 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/compression/zlib/ZlibXmppCompressionFactory.java @@ -0,0 +1,287 @@ +/** + * + * Copyright 2018 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.compression.zlib; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.zip.DataFormatException; +import java.util.zip.Deflater; +import java.util.zip.Inflater; + +import org.jivesoftware.smack.ConnectionConfiguration; +import org.jivesoftware.smack.XmppInputOutputFilter; +import org.jivesoftware.smack.compression.XMPPInputOutputStream; +import org.jivesoftware.smack.compression.XMPPInputOutputStream.FlushMethod; +import org.jivesoftware.smack.compression.XmppCompressionFactory; + +public final class ZlibXmppCompressionFactory extends XmppCompressionFactory { + + public static final ZlibXmppCompressionFactory INSTANCE = new ZlibXmppCompressionFactory(); + + private ZlibXmppCompressionFactory() { + super("zlib", 100); + } + + @Override + public XmppInputOutputFilter fabricate(ConnectionConfiguration configuration) { + return new ZlibXmppInputOutputFilter(); + } + + private static final class ZlibXmppInputOutputFilter implements XmppInputOutputFilter { + + private static final int MINIMUM_OUTPUT_BUFFER_INITIAL_SIZE = 4; + private static final int MINIMUM_OUTPUT_BUFFER_INCREASE = 480; + + private final Deflater compressor; + private final Inflater decompressor = new Inflater(); + + private long compressorInBytes; + private long compressorOutBytes; + + private long decompressorInBytes; + private long decompressorOutBytes; + + private int maxOutputOutput = -1; + private int maxInputOutput = -1; + + private int maxBytesWrittenAfterFullFlush = -1; + + private ZlibXmppInputOutputFilter() { + this(Deflater.DEFAULT_COMPRESSION); + } + + private ZlibXmppInputOutputFilter(int compressionLevel) { + compressor = new Deflater(compressionLevel); + } + + private ByteBuffer outputBuffer; + + @Override + public OutputResult output(ByteBuffer outputData, boolean isFinalDataOfElement, boolean destinationAddressChanged, + boolean moreDataAvailable) throws IOException { + if (destinationAddressChanged && XMPPInputOutputStream.getFlushMethod() == FlushMethod.FULL_FLUSH) { + outputBuffer = ByteBuffer.allocate(256); + + int bytesWritten = deflate(Deflater.FULL_FLUSH); + + maxBytesWrittenAfterFullFlush = Math.max(bytesWritten, maxBytesWrittenAfterFullFlush); + compressorOutBytes += bytesWritten; + } + + if (outputData == null && outputBuffer == null) { + return OutputResult.NO_OUTPUT; + } + + int bytesRemaining = outputData.remaining(); + if (outputBuffer == null) { + final int outputBufferSize = bytesRemaining < MINIMUM_OUTPUT_BUFFER_INITIAL_SIZE ? MINIMUM_OUTPUT_BUFFER_INITIAL_SIZE : bytesRemaining; + // We assume that the compressed data will not take more space as the uncompressed. Even if this is not + // always true, the automatic buffer resize mechanism of deflate() will take care. + outputBuffer = ByteBuffer.allocate(outputBufferSize); + } + + // There is an invariant of Deflater/Inflater that input should only be set if needsInput() return true. + assert (compressor.needsInput()); + + final byte[] compressorInputBuffer; + final int compressorInputBufferOffset, compressorInputBufferLength; + if (outputData.hasArray()) { + compressorInputBuffer = outputData.array(); + compressorInputBufferOffset = outputData.arrayOffset(); + compressorInputBufferLength = outputData.remaining(); + } else { + compressorInputBuffer = new byte[outputData.remaining()]; + compressorInputBufferOffset = 0; + compressorInputBufferLength = compressorInputBuffer.length; + outputData.get(compressorInputBuffer); + } + + compressorInBytes += compressorInputBufferLength; + + compressor.setInput(compressorInputBuffer, compressorInputBufferOffset, compressorInputBufferLength); + + int flushMode; + if (moreDataAvailable) { + flushMode = Deflater.NO_FLUSH; + } else { + flushMode = Deflater.SYNC_FLUSH; + } + + int bytesWritten = deflate(flushMode); + + maxOutputOutput = Math.max(outputBuffer.position(), maxOutputOutput); + compressorOutBytes += bytesWritten; + + OutputResult outputResult = new OutputResult(outputBuffer); + outputBuffer = null; + return outputResult; + } + + private int deflate(int flushMode) { +// compressor.finish(); + + int totalBytesWritten = 0; + while (true) { + int initialOutputBufferPosition = outputBuffer.position(); + byte[] buffer = outputBuffer.array(); + int length = outputBuffer.limit() - initialOutputBufferPosition; + + int bytesWritten = compressor.deflate(buffer, initialOutputBufferPosition, length, flushMode); + + int newOutputBufferPosition = initialOutputBufferPosition + bytesWritten; + outputBuffer.position(newOutputBufferPosition); + + totalBytesWritten += bytesWritten; + + if (compressor.needsInput() && outputBuffer.hasRemaining()) { + break; + } + + int increasedBufferSize = outputBuffer.capacity() * 2; + if (increasedBufferSize < MINIMUM_OUTPUT_BUFFER_INCREASE) { + increasedBufferSize = MINIMUM_OUTPUT_BUFFER_INCREASE; + } + ByteBuffer newCurrentOutputBuffer = ByteBuffer.allocate(increasedBufferSize); + outputBuffer.flip(); + newCurrentOutputBuffer.put(outputBuffer); + outputBuffer = newCurrentOutputBuffer; + } + + return totalBytesWritten; + } + + @Override + public ByteBuffer input(ByteBuffer inputData) throws IOException { + int bytesRemaining = inputData.remaining(); + + final byte[] inputBytes; + final int offset, length; + if (inputData.hasArray()) { + inputBytes = inputData.array(); + offset = inputData.arrayOffset(); + length = inputData.remaining(); + } else { + // Copy since we are dealing with a buffer whose array is not accessible (possibly a direct buffer). + inputBytes = new byte[bytesRemaining]; + inputData.get(inputBytes); + offset = 0; + length = inputBytes.length; + } + + decompressorInBytes += length; + + decompressor.setInput(inputBytes, offset, length); + + int bytesInflated; + // Assume that the inflated/decompressed result will be roughly at most twice the size of the compressed + // variant. It appears to hold most of the times, if not, then the buffer resize mechanism will take care of + // it. + ByteBuffer outputBuffer = ByteBuffer.allocate(2 * length); + while (true) { + byte[] inflateOutputBuffer = outputBuffer.array(); + int inflateOutputBufferOffset = outputBuffer.position(); + int inflateOutputBufferLength = outputBuffer.limit() - inflateOutputBufferOffset; + try { + bytesInflated = decompressor.inflate(inflateOutputBuffer, inflateOutputBufferOffset, inflateOutputBufferLength); + } + catch (DataFormatException e) { + throw new IOException(e); + } + + outputBuffer.position(inflateOutputBufferOffset + bytesInflated); + + decompressorOutBytes += bytesInflated; + + if (decompressor.needsInput()) { + break; + } + + int increasedBufferSize = outputBuffer.capacity() * 2; + ByteBuffer increasedOutputBuffer = ByteBuffer.allocate(increasedBufferSize); + outputBuffer.flip(); + increasedOutputBuffer.put(outputBuffer); + outputBuffer = increasedOutputBuffer; + } + + if (bytesInflated == 0) { + return null; + } + + maxInputOutput = Math.max(outputBuffer.position(), maxInputOutput); + + return outputBuffer; + } + + @Override + public Stats getStats() { + return new Stats(this); + } + } + + public static final class Stats { + public final long compressorInBytes; + public final long compressorOutBytes; + public final double compressionRatio; + + public final long decompressorInBytes; + public final long decompressorOutBytes; + public final double decompressionRatio; + + public final int maxOutputOutput; + public final int maxInputOutput; + + public final int maxBytesWrittenAfterFullFlush; + + private Stats(ZlibXmppInputOutputFilter filter) { + // Note that we read the out bytes before the in bytes to not over approximate the compression ratio. + compressorOutBytes = filter.compressorOutBytes; + compressorInBytes = filter.compressorInBytes; + compressionRatio = (double) compressorOutBytes / compressorInBytes; + + decompressorOutBytes = filter.decompressorOutBytes; + decompressorInBytes = filter.decompressorInBytes; + decompressionRatio = (double) decompressorInBytes / decompressorOutBytes; + + maxOutputOutput = filter.maxOutputOutput; + maxInputOutput = filter.maxInputOutput; + maxBytesWrittenAfterFullFlush = filter.maxBytesWrittenAfterFullFlush; + } + + private transient String toStringCache; + + @Override + public String toString() { + if (toStringCache != null) { + return toStringCache; + } + + toStringCache = + "compressor-in-bytes: " + compressorInBytes + '\n' + + "compressor-out-bytes: " + compressorOutBytes + '\n' + + "compression-ratio: " + compressionRatio + '\n' + + "decompressor-in-bytes: " + decompressorInBytes + '\n' + + "decompressor-out-bytes: " + decompressorOutBytes + '\n' + + "decompression-ratio: " + decompressionRatio + '\n' + + "max-output-output: " + maxOutputOutput + '\n' + + "max-input-output: " + maxInputOutput + '\n' + + "max-bytes-written-after-full-flush: " + maxBytesWrittenAfterFullFlush + '\n' + ; + + return toStringCache; + } + } +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/compression/zlib/package-info.java b/smack-core/src/main/java/org/jivesoftware/smack/compression/zlib/package-info.java new file mode 100644 index 000000000..d229bfe8d --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/compression/zlib/package-info.java @@ -0,0 +1,21 @@ +/** + * + * 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. + * 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. + */ + +/** + * Smack classes for compression (XEP-0138) using zlib (RFC 1950). + */ +package org.jivesoftware.smack.compression.zlib; diff --git a/smack-core/src/main/java/org/jivesoftware/smack/debugger/AbstractDebugger.java b/smack-core/src/main/java/org/jivesoftware/smack/debugger/AbstractDebugger.java index 25dc98899..e1fc4f2a0 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/debugger/AbstractDebugger.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/debugger/AbstractDebugger.java @@ -16,8 +16,6 @@ */ package org.jivesoftware.smack.debugger; -import java.io.Reader; -import java.io.Writer; import java.util.logging.Logger; import org.jivesoftware.smack.AbstractConnectionListener; @@ -133,21 +131,13 @@ public abstract class AbstractDebugger extends SmackDebugger { protected abstract void log(String logMessage, Throwable throwable); @Override - public Reader newConnectionReader(Reader newReader) { - reader.removeReaderListener(readerListener); - ObservableReader debugReader = new ObservableReader(newReader); - debugReader.addReaderListener(readerListener); - reader = debugReader; - return reader; + public final void outgoingStreamSink(CharSequence outgoingCharSequence) { + log("SENT (" + connection.getConnectionCounter() + "): " + outgoingCharSequence); } @Override - public Writer newConnectionWriter(Writer newWriter) { - writer.removeWriterListener(writerListener); - ObservableWriter debugWriter = new ObservableWriter(newWriter); - debugWriter.addWriterListener(writerListener); - writer = debugWriter; - return writer; + public final void incomingStreamSink(CharSequence incomingCharSequence) { + log("RECV (" + connection.getConnectionCounter() + "): " + incomingCharSequence); } @Override diff --git a/smack-core/src/main/java/org/jivesoftware/smack/debugger/SmackDebugger.java b/smack-core/src/main/java/org/jivesoftware/smack/debugger/SmackDebugger.java index 23b07785d..3c4410348 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/debugger/SmackDebugger.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/debugger/SmackDebugger.java @@ -17,13 +17,18 @@ package org.jivesoftware.smack.debugger; +import java.io.IOException; import java.io.Reader; import java.io.Writer; import org.jivesoftware.smack.XMPPConnection; import org.jivesoftware.smack.packet.TopLevelStreamElement; +import org.jivesoftware.smack.util.ObservableReader; +import org.jivesoftware.smack.util.ObservableWriter; import org.jxmpp.jid.EntityFullJid; +import org.jxmpp.xml.splitter.XmlPrettyPrinter; +import org.jxmpp.xml.splitter.XmppXmlSplitter; /** * Interface that allows for implementing classes to debug XML traffic. That is a GUI window that @@ -38,6 +43,9 @@ public abstract class SmackDebugger { protected final XMPPConnection connection; + private XmppXmlSplitter outgoingStreamSplitterForPrettyPrinting; + private XmppXmlSplitter incomingStreamSplitterForPrettyPrinting; + protected SmackDebugger(XMPPConnection connection) { this.connection = connection; } @@ -52,6 +60,21 @@ public abstract class SmackDebugger { // TODO: Should be replaced with a connection listener authenticed(). public abstract void userHasLogged(EntityFullJid user); + /** + * Note that the sequence of characters may be pretty printed. + * + * @param outgoingCharSequence the outgoing character sequence. + */ + public abstract void outgoingStreamSink(CharSequence outgoingCharSequence); + + public void onOutgoingElementCompleted() { + } + + public abstract void incomingStreamSink(CharSequence incomingCharSequence); + + public void onIncomingElementCompleted() { + } + /** * Returns a new special Reader that wraps the new connection Reader. The connection * has been secured so the connection is using a new reader and writer. The debugger @@ -61,7 +84,23 @@ public abstract class SmackDebugger { * @param reader connection reader. * @return a new special Reader that wraps the new connection Reader. */ - public abstract Reader newConnectionReader(Reader reader); + public final Reader newConnectionReader(Reader reader) { + XmlPrettyPrinter xmlPrettyPrinter = XmlPrettyPrinter.builder() + .setPrettyWriter((sb) -> incomingStreamSink(sb)) + .build(); + incomingStreamSplitterForPrettyPrinting = new XmppXmlSplitter(xmlPrettyPrinter); + + ObservableReader observableReader = new ObservableReader(reader); + observableReader.addReaderListener((readString) -> { + try { + incomingStreamSplitterForPrettyPrinting.append(readString); + } + catch (IOException e) { + throw new AssertionError(e); + } + }); + return observableReader; + } /** * Returns a new special Writer that wraps the new connection Writer. The connection @@ -72,7 +111,23 @@ public abstract class SmackDebugger { * @param writer connection writer. * @return a new special Writer that wraps the new connection Writer. */ - public abstract Writer newConnectionWriter(Writer writer); + public final Writer newConnectionWriter(Writer writer) { + XmlPrettyPrinter xmlPrettyPrinter = XmlPrettyPrinter.builder() + .setPrettyWriter((sb) -> outgoingStreamSink(sb)) + .build(); + outgoingStreamSplitterForPrettyPrinting = new XmppXmlSplitter(xmlPrettyPrinter); + + ObservableWriter observableWriter = new ObservableWriter(writer); + observableWriter.addWriterListener((writtenString) -> { + try { + outgoingStreamSplitterForPrettyPrinting.append(writtenString); + } + catch (IOException e) { + throw new AssertionError(e); + } + }); + return observableWriter; + } /** * Used by the connection to notify about an incoming top level stream element. diff --git a/smack-core/src/main/java/org/jivesoftware/smack/fsm/AbstractXmppStateMachineConnection.java b/smack-core/src/main/java/org/jivesoftware/smack/fsm/AbstractXmppStateMachineConnection.java new file mode 100644 index 000000000..9bf3f0033 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/fsm/AbstractXmppStateMachineConnection.java @@ -0,0 +1,816 @@ +/** + * + * Copyright 2018-2019 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.fsm; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; + +import javax.net.ssl.SSLSession; + +import org.jivesoftware.smack.AbstractXMPPConnection; +import org.jivesoftware.smack.ConnectionConfiguration; +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.XMPPException; +import org.jivesoftware.smack.XMPPException.FailedNonzaException; +import org.jivesoftware.smack.XMPPException.StreamErrorException; +import org.jivesoftware.smack.XMPPException.XMPPErrorException; +import org.jivesoftware.smack.XmppInputOutputFilter; +import org.jivesoftware.smack.compress.packet.Compress; +import org.jivesoftware.smack.compress.packet.Compressed; +import org.jivesoftware.smack.compress.packet.Failure; +import org.jivesoftware.smack.compression.XmppCompressionFactory; +import org.jivesoftware.smack.compression.XmppCompressionManager; +import org.jivesoftware.smack.fsm.StateDescriptorGraph.GraphVertex; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.packet.Presence; +import org.jivesoftware.smack.packet.StreamError; +import org.jivesoftware.smack.sasl.SASLErrorException; +import org.jivesoftware.smack.sasl.SASLMechanism; +import org.jivesoftware.smack.sasl.packet.SaslStreamElements.Challenge; +import org.jivesoftware.smack.sasl.packet.SaslStreamElements.Success; +import org.jivesoftware.smack.util.Objects; +import org.jivesoftware.smack.util.PacketParserUtils; + +import org.jxmpp.jid.parts.Resourcepart; +import org.xmlpull.v1.XmlPullParser; + +public abstract class AbstractXmppStateMachineConnection extends AbstractXMPPConnection { + + private final List connectionStateMachineListeners = new CopyOnWriteArrayList<>(); + + private boolean featuresReceived; + + protected boolean streamResumed; + + private GraphVertex currentStateVertex; + + private List walkFromDisconnectToAuthenticated; + + private final List inputOutputFilters = new CopyOnWriteArrayList<>(); + private List previousInputOutputFilters; + + protected AbstractXmppStateMachineConnection(ConnectionConfiguration configuration, GraphVertex initialStateDescriptorVertex) { + super(configuration); + currentStateVertex = StateDescriptorGraph.convertToStateGraph(initialStateDescriptorVertex, this); + } + + @Override + protected void loginInternal(String username, String password, Resourcepart resource) + throws XMPPException, SmackException, IOException, InterruptedException { + WalkStateGraphContext walkStateGraphContext = buildNewWalkTo(AuthenticatedAndResourceBoundStateDescriptor.class) + .withLoginContext(username, password, resource) + .build(); + walkStateGraph(walkStateGraphContext); + } + + protected static WalkStateGraphContextBuilder buildNewWalkTo(Class finalStateClass) { + return new WalkStateGraphContextBuilder(finalStateClass); + } + + protected static final class WalkStateGraphContext { + private final Class finalStateClass; + private final Class mandatoryIntermediateState; + private final LoginContext loginContext; + + private final List walkedStateGraphPath = new ArrayList<>(); + + /** + * A linked Map of failed States with their reason as value. + */ + private final Map failedStates = new LinkedHashMap<>(); + + private boolean mandatoryIntermediateStateHandled; + + private WalkStateGraphContext(Class finalStateClass, Class mandatoryIntermedidateState, LoginContext loginContext) { + this.finalStateClass = Objects.requireNonNull(finalStateClass); + this.mandatoryIntermediateState = mandatoryIntermedidateState; + this.loginContext = loginContext; + } + + public boolean isFinalStateAuthenticatedAndResourceBound() { + return finalStateClass == AuthenticatedAndResourceBoundStateDescriptor.class; + } + } + + protected static final class WalkStateGraphContextBuilder { + private final Class finalStateClass; + private Class mandatoryIntermedidateState; + private LoginContext loginContext; + + private WalkStateGraphContextBuilder(Class finalStateClass) { + this.finalStateClass = finalStateClass; + } + + public WalkStateGraphContextBuilder withMandatoryIntermediateState(Class mandatoryIntermedidateState) { + this.mandatoryIntermedidateState = mandatoryIntermedidateState; + return this; + } + + public WalkStateGraphContextBuilder withLoginContext(String username, String password, Resourcepart resource) { + LoginContext loginContext = new LoginContext(username, password, resource); + return withLoginContext(loginContext); + } + + public WalkStateGraphContextBuilder withLoginContext(LoginContext loginContext) { + this.loginContext = loginContext; + return this; + } + + public WalkStateGraphContext build() { + return new WalkStateGraphContext(finalStateClass, mandatoryIntermedidateState, loginContext); + } + } + + protected final 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 (XMPPErrorException | SASLErrorException | FailedNonzaException | IOException | SmackException + | InterruptedException e) { + currentStateVertex = previousStateVertex; + // Reset that state. + State revertedState = currentStateVertex.getElement(); + invokeConnectionStateMachineListener(new ConnectionStateEvent.StateRevertBackwardsWalk(revertedState)); + revertedState.resetState(); + throw e; + } + } + + 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(); + final StateDescriptor initialStateDescriptor = initialState.getStateDescriptor(); + + walkStateGraphContext.walkedStateGraphPath.add(initialState); + + if (initialStateDescriptor.getClass() == walkStateGraphContext.finalStateClass) { + // If this is used as final state, then it should be marked as such. + assert (initialStateDescriptor.isFinalState()); + + // We reached the final state. + invokeConnectionStateMachineListener(new ConnectionStateEvent.FinalStateReached(initialState)); + return; + } + + + List> outgoingStateEdges = currentStateVertex.getOutgoingEdges(); + + // See if we need to handle mandatory intermediate states. + if (walkStateGraphContext.mandatoryIntermediateState != null && !walkStateGraphContext.mandatoryIntermediateStateHandled) { + // Check if outgoingStateEdges contains the mandatory intermediate state. + GraphVertex mandatoryIntermediateStateVertex = null; + for (GraphVertex outgoingStateVertex : outgoingStateEdges) { + if (outgoingStateVertex.getElement().getStateDescriptor().getClass() == walkStateGraphContext.mandatoryIntermediateState) { + mandatoryIntermediateStateVertex = outgoingStateVertex; + break; + } + } + + if (mandatoryIntermediateStateVertex != null) { + walkStateGraphContext.mandatoryIntermediateStateHandled = true; + TransitionReason reason = attemptEnterState(mandatoryIntermediateStateVertex, walkStateGraphContext); + if (reason instanceof TransitionSuccessResult) { + walkStateGraph(walkStateGraphContext); + return; + } + + // We could not enter a mandatory intermediate state. Throw here. + throw new StateMachineException.SmackMandatoryStateFailedException( + mandatoryIntermediateStateVertex.getElement(), reason); + } + } + + for (Iterator> it = outgoingStateEdges.iterator(); it.hasNext();) { + GraphVertex successorStateVertex = it.next(); + State successorState = successorStateVertex.getElement(); + TransitionReason reason = attemptEnterState(successorStateVertex, walkStateGraphContext); + if (reason instanceof TransitionSuccessResult) { + break; + } + + // If attemptEnterState did not throw and did not return a value of type TransitionSuccessResult, then we + // just record this value and go on from there. Note that reason may be null, which is returned by + // attemptEnterState in case the state was already successfully handled. If this is the case, then we don't + // record it. + if (reason != null) { + walkStateGraphContext.failedStates.put(successorState, reason); + } + + if (!it.hasNext()) { + throw new StateMachineException.SmackStateGraphDeadEndException(walkStateGraphContext.walkedStateGraphPath, walkStateGraphContext.failedStates); + } + } + + // Walk the state graph by recursion. + walkStateGraph(walkStateGraphContext); + } + + private TransitionReason attemptEnterState(GraphVertex successorStateVertex, + WalkStateGraphContext walkStateGraphContext) + throws SmackException, XMPPErrorException, SASLErrorException, IOException, InterruptedException, FailedNonzaException { + final State successorState = successorStateVertex.getElement(); + final StateDescriptor successorStateDescriptor = successorState.getStateDescriptor(); + + if (!successorStateDescriptor.isMultiVisitState() && walkStateGraphContext.walkedStateGraphPath.contains(successorState)) { + // This can happen if a state leads back to the state where it originated from. See for example the + // 'Compression' state. We return 'null' here to signal that the state can safely be ignored. + return null; + } + + if (successorStateDescriptor.isNotImplemented()) { + TransitionImpossibleBecauseNotImplemented transtionImpossibleBecauseNotImplemented = new TransitionImpossibleBecauseNotImplemented( + successorStateDescriptor); + invokeConnectionStateMachineListener(new ConnectionStateEvent.TransitionNotPossible(successorState, + transtionImpossibleBecauseNotImplemented)); + return transtionImpossibleBecauseNotImplemented; + } + + final TransitionIntoResult transitionIntoResult; + try { + TransitionImpossibleReason transitionImpossibleReason = successorState.isTransitionToPossible(walkStateGraphContext); + if (transitionImpossibleReason != null) { + invokeConnectionStateMachineListener(new ConnectionStateEvent.TransitionNotPossible(successorState, + transitionImpossibleReason)); + return transitionImpossibleReason; + } + + invokeConnectionStateMachineListener(new ConnectionStateEvent.AboutToTransitionInto(successorState)); + transitionIntoResult = successorState.transitionInto(walkStateGraphContext); + } catch (SmackException | XMPPErrorException | SASLErrorException | IOException | InterruptedException + | FailedNonzaException e) { + // TODO Document why this is required given that there is another call site of resetState(). + invokeConnectionStateMachineListener(new ConnectionStateEvent.StateRevertBackwardsWalk(successorState)); + successorState.resetState(); + throw e; + } + if (transitionIntoResult instanceof TransitionFailureResult) { + TransitionFailureResult transitionFailureResult = (TransitionFailureResult) transitionIntoResult; + invokeConnectionStateMachineListener(new ConnectionStateEvent.TransitionFailed(successorState, transitionFailureResult)); + return transitionIntoResult; + } + + // If transitionIntoResult is not an instance of TransitionFailureResult, then it has to be of type + // TransitionSuccessResult. + TransitionSuccessResult transitionSuccessResult = (TransitionSuccessResult) transitionIntoResult; + + currentStateVertex = successorStateVertex; + invokeConnectionStateMachineListener(new ConnectionStateEvent.SuccessfullyTransitionedInto(successorState, + transitionSuccessResult)); + + return transitionSuccessResult; + } + + protected abstract SSLSession getSSLSession(); + + @Override + protected void afterFeaturesReceived() { + featuresReceived = true; + synchronized (this) { + notifyAll(); + } + } + + protected final void parseAndProcessElement(String element) throws Exception { + XmlPullParser parser = PacketParserUtils.getParserFor(element); + + // Skip the enclosing stream open what is guaranteed to be there. + parser.next(); + + int event = parser.getEventType(); + outerloop: while (true) { + switch (event) { + case XmlPullParser.START_TAG: + final String name = parser.getName(); + // Note that we don't handle "stream" here as it's done in the splitter. + switch (name) { + case Message.ELEMENT: + case IQ.IQ_ELEMENT: + case Presence.ELEMENT: + try { + parseAndProcessStanza(parser); + } finally { + // TODO: Here would be the following stream management code. + // clientHandledStanzasCount = SMUtils.incrementHeight(clientHandledStanzasCount); + } + break; + case "error": + StreamError streamError = PacketParserUtils.parseStreamError(parser); + saslFeatureReceived.reportFailure(new StreamErrorException(streamError)); + throw new StreamErrorException(streamError); + case "features": + parseFeatures(parser); + afterFeaturesReceived(); + break; + // SASL related top level stream elements + case Challenge.ELEMENT: + // The server is challenging the SASL authentication made by the client + String challengeData = parser.nextText(); + getSASLAuthentication().challengeReceived(challengeData); + break; + case Success.ELEMENT: + Success success = new Success(parser.nextText()); + // The SASL authentication with the server was successful. The next step + // will be to bind the resource + getSASLAuthentication().authenticated(success); + sendStreamOpen(); + break; + default: + parseAndProcessNonza(parser); + break; + } + break; + case XmlPullParser.END_DOCUMENT: + break outerloop; + } + event = parser.next(); + } + } + + protected synchronized void prepareToWaitForFeaturesReceived() { + featuresReceived = false; + } + + protected void waitForFeaturesReceived(String 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, + ConnectionUnexpectedTerminatedException, NoResponseException, NotConnectedException { + prepareToWaitForFeaturesReceived(); + sendStreamOpen(); + waitForFeaturesReceived(waitFor); + } + + protected final void addXmppInputOutputFilter(XmppInputOutputFilter xmppInputOutputFilter) { + inputOutputFilters.add(0, xmppInputOutputFilter); + } + + protected final ListIterator getXmppInputOutputFilterBeginIterator() { + return inputOutputFilters.listIterator(); + } + + protected final ListIterator getXmppInputOutputFilterEndIterator() { + return inputOutputFilters.listIterator(inputOutputFilters.size()); + } + + protected final synchronized List getFilterStats() { + Collection filters; + if (inputOutputFilters.isEmpty() && previousInputOutputFilters != null) { + filters = previousInputOutputFilters; + } else { + filters = inputOutputFilters; + } + + List filterStats = new ArrayList<>(filters.size()); + for (XmppInputOutputFilter xmppInputOutputFilter : filters) { + Object stats = xmppInputOutputFilter.getStats(); + if (stats != null) { + filterStats.add(stats); + } + } + + return Collections.unmodifiableList(filterStats); + } + + protected abstract class State { + private final StateDescriptor stateDescriptor; + + protected State(StateDescriptor stateDescriptor) { + this.stateDescriptor = stateDescriptor; + } + + /** + * Check if the state should be activated. + * + * @return null if the state should be activated. + * @throws SmackException in case a Smack exception occurs. + */ + protected TransitionImpossibleReason isTransitionToPossible(WalkStateGraphContext walkStateGraphContext) throws SmackException { + return null; + } + + protected abstract TransitionIntoResult transitionInto(WalkStateGraphContext walkStateGraphContext) + throws XMPPErrorException, SASLErrorException, IOException, SmackException, InterruptedException, FailedNonzaException; + + StateDescriptor getStateDescriptor() { + return stateDescriptor; + } + + protected void resetState() { + } + + @Override + public String toString() { + return "State " + stateDescriptor + ' ' + AbstractXmppStateMachineConnection.this; + } + + protected final void ensureNotOnOurWayToAuthenticatedAndResourceBound(WalkStateGraphContext walkStateGraphContext) { + if (walkStateGraphContext.isFinalStateAuthenticatedAndResourceBound()) { + throw new IllegalStateException( + "Smack should never attempt to reach the authenticated and resource bound state over " + this + + ". This is probably a programming error within Smack, please report it to the develoeprs."); + } + } + } + + abstract static class TransitionReason { + public final String reason; + private TransitionReason(String reason) { + this.reason = reason; + } + + @Override + public final String toString() { + return reason; + } + } + + protected static class TransitionImpossibleReason extends TransitionReason { + public TransitionImpossibleReason(String reason) { + super(reason); + } + } + + protected static class TransitionImpossibleBecauseNotImplemented extends TransitionImpossibleReason { + public TransitionImpossibleBecauseNotImplemented(StateDescriptor stateDescriptor) { + super(stateDescriptor.getFullStateName(false) + " is not implemented (yet)"); + } + } + + protected abstract static class TransitionIntoResult extends TransitionReason { + public TransitionIntoResult(String reason) { + super(reason); + } + } + + public static class TransitionSuccessResult extends TransitionIntoResult { + + public static final TransitionSuccessResult EMPTY_INSTANCE = new TransitionSuccessResult(); + + private TransitionSuccessResult() { + super(""); + } + + public TransitionSuccessResult(String reason) { + super(reason); + } + } + + public static final class TransitionFailureResult extends TransitionIntoResult { + private TransitionFailureResult(String reason) { + super(reason); + } + } + + protected final class NoOpState extends State { + + private NoOpState(StateDescriptor stateDescriptor) { + super(stateDescriptor); + } + + @Override + protected TransitionImpossibleReason isTransitionToPossible(WalkStateGraphContext walkStateGraphContext) { + // Transition into a NoOpState is always possible. + return null; + } + + @Override + protected TransitionIntoResult transitionInto(WalkStateGraphContext walkStateGraphContext) { + // Transition into a NoOpState always succeeds. + return TransitionSuccessResult.EMPTY_INSTANCE; + } + } + + protected static class DisconnectedStateDescriptor extends StateDescriptor { + protected DisconnectedStateDescriptor() { + super(DisconnectedState.class, StateDescriptor.Property.finalState); + } + } + + private final class DisconnectedState extends State { + + private DisconnectedState(StateDescriptor stateDescriptor) { + super(stateDescriptor); + } + + @Override + protected TransitionIntoResult transitionInto(WalkStateGraphContext walkStateGraphContext) { + if (inputOutputFilters.isEmpty()) { + previousInputOutputFilters = null; + } else { + previousInputOutputFilters = new ArrayList<>(inputOutputFilters.size()); + previousInputOutputFilters.addAll(inputOutputFilters); + inputOutputFilters.clear(); + } + + ListIterator it = walkFromDisconnectToAuthenticated.listIterator( + walkFromDisconnectToAuthenticated.size()); + while (it.hasPrevious()) { + State stateToReset = it.previous(); + stateToReset.resetState(); + } + walkFromDisconnectToAuthenticated = null; + + return TransitionSuccessResult.EMPTY_INSTANCE; + } + } + + protected static final class ConnectedButUnauthenticatedStateDescriptor extends StateDescriptor { + private ConnectedButUnauthenticatedStateDescriptor() { + super(ConnectedButUnauthenticatedState.class, StateDescriptor.Property.finalState); + addSuccessor(SaslAuthenticationStateDescriptor.class); + } + } + + private final class ConnectedButUnauthenticatedState extends State { + private ConnectedButUnauthenticatedState(StateDescriptor stateDescriptor) { + super(stateDescriptor); + } + + @Override + protected TransitionIntoResult transitionInto(WalkStateGraphContext walkStateGraphContext) { + assert (walkFromDisconnectToAuthenticated == null); + if (getStateDescriptor().getClass() == walkStateGraphContext.finalStateClass) { + // If this is the final state, then record the walk so far. + walkFromDisconnectToAuthenticated = new ArrayList<>(walkStateGraphContext.walkedStateGraphPath); + } + + connected = true; + return TransitionSuccessResult.EMPTY_INSTANCE; + } + + @Override + protected void resetState() { + connected = false; + } + } + + protected static final class SaslAuthenticationStateDescriptor extends StateDescriptor { + private SaslAuthenticationStateDescriptor() { + super(SaslAuthenticationState.class, "RFC 6120 ยง 6"); + addSuccessor(AuthenticatedButUnboundStateDescriptor.class); + } + } + + private final class SaslAuthenticationState extends State { + private SaslAuthenticationState(StateDescriptor stateDescriptor) { + super(stateDescriptor); + } + + @Override + protected TransitionIntoResult transitionInto(WalkStateGraphContext walkStateGraphContext) throws XMPPErrorException, + SASLErrorException, IOException, SmackException, InterruptedException { + prepareToWaitForFeaturesReceived(); + + LoginContext loginContext = walkStateGraphContext.loginContext; + SASLMechanism usedSaslMechanism = saslAuthentication.authenticate(loginContext.username, loginContext.password, config.getAuthzid(), getSSLSession()); + // authenticate() will only return if the SASL authentication was successful, but we also need to wait for the next round of stream features. + + waitForFeaturesReceived("server stream features after SASL authentication"); + + return new SaslAuthenticationSuccessResult(usedSaslMechanism); + } + } + + public static final class SaslAuthenticationSuccessResult extends TransitionSuccessResult { + private final String saslMechanismName; + + private SaslAuthenticationSuccessResult(SASLMechanism usedSaslMechanism) { + super("SASL authentication successfull using " + usedSaslMechanism.getName()); + this.saslMechanismName = usedSaslMechanism.getName(); + } + + public String getSaslMechanismName() { + return saslMechanismName; + } + } + + protected static final class AuthenticatedButUnboundStateDescriptor extends StateDescriptor { + private AuthenticatedButUnboundStateDescriptor() { + super(StateDescriptor.Property.multiVisitState); + addSuccessor(ResourceBindingStateDescriptor.class); + addSuccessor(CompressionStateDescriptor.class); + } + } + + protected static final class ResourceBindingStateDescriptor extends StateDescriptor { + private ResourceBindingStateDescriptor() { + super(ResourceBindingState.class, "RFC 6120 ยง 7"); + addSuccessor(AuthenticatedAndResourceBoundStateDescriptor.class); + } + } + + private final class ResourceBindingState extends State { + private ResourceBindingState(StateDescriptor stateDescriptor) { + super(stateDescriptor); + } + + @Override + protected TransitionIntoResult transitionInto(WalkStateGraphContext walkStateGraphContext) throws XMPPErrorException, + SASLErrorException, IOException, SmackException, InterruptedException { + // TODO: The reportSuccess() is just a quick fix until there is a variant of the + // bindResourceAndEstablishSession() method which does not require this. + lastFeaturesReceived.reportSuccess(); + + LoginContext loginContext = walkStateGraphContext.loginContext; + Resourcepart resource = bindResourceAndEstablishSession(loginContext.resource); + streamResumed = false; + + return new ResourceBoundResult(resource, loginContext.resource); + } + } + + public static final class ResourceBoundResult extends TransitionSuccessResult { + private final Resourcepart resource; + + private ResourceBoundResult(Resourcepart boundResource, Resourcepart requestedResource) { + super("Resource '" + boundResource + "' bound (requested: '" + requestedResource + "'"); + this.resource = boundResource; + } + + public Resourcepart getResource() { + return resource; + } + } + + protected static final class CompressionStateDescriptor extends StateDescriptor { + private CompressionStateDescriptor() { + super(CompressionState.class, 138); + addSuccessor(AuthenticatedButUnboundStateDescriptor.class); + declarePrecedenceOver(ResourceBindingStateDescriptor.class); + } + } + + private boolean compressionEnabled; + + private class CompressionState extends State { + private XmppCompressionFactory selectedCompressionFactory; + private XmppInputOutputFilter usedXmppInputOutputCompressionFitler; + + protected CompressionState(StateDescriptor stateDescriptor) { + super(stateDescriptor); + } + + @Override + protected TransitionImpossibleReason isTransitionToPossible(WalkStateGraphContext walkStateGraphContext) { + if (!config.isCompressionEnabled()) { + return new TransitionImpossibleReason("Stream compression disabled"); + } + + Compress.Feature compressFeature = getFeature(Compress.Feature.ELEMENT, Compress.NAMESPACE); + if (compressFeature == null) { + return new TransitionImpossibleReason("Stream compression not supported"); + } + + selectedCompressionFactory = XmppCompressionManager.getBestFactory(compressFeature); + if (selectedCompressionFactory == null) { + return new TransitionImpossibleReason("No matching compression factory"); + } + + usedXmppInputOutputCompressionFitler = selectedCompressionFactory.fabricate(config); + + return null; + } + + @Override + protected TransitionIntoResult transitionInto(WalkStateGraphContext walkStateGraphContext) + throws NoResponseException, NotConnectedException, FailedNonzaException, InterruptedException, + ConnectionUnexpectedTerminatedException { + final String compressionMethod = selectedCompressionFactory.getCompressionMethod(); + sendAndWaitForResponse(new Compress(compressionMethod), Compressed.class, Failure.class); + + addXmppInputOutputFilter(usedXmppInputOutputCompressionFitler); + + newStreamOpenWaitForFeaturesSequence("server stream features after compression enabled"); + + compressionEnabled = true; + + return new CompressionTransitionSuccessResult(compressionMethod); + } + + @Override + protected void resetState() { + selectedCompressionFactory = null; + usedXmppInputOutputCompressionFitler = null; + compressionEnabled = false; + } + } + + public static final class CompressionTransitionSuccessResult extends TransitionSuccessResult { + private final String compressionMethod; + + private CompressionTransitionSuccessResult(String compressionMethod) { + super(compressionMethod + " compression enabled"); + this.compressionMethod = compressionMethod; + } + + public String getCompressionMethod() { + return compressionMethod; + } + } + + @Override + public final boolean isUsingCompression() { + return compressionEnabled; + } + + protected static final class AuthenticatedAndResourceBoundStateDescriptor extends StateDescriptor { + private AuthenticatedAndResourceBoundStateDescriptor() { + super(AuthenticatedAndResourceBoundState.class, StateDescriptor.Property.finalState); + } + } + + private final class AuthenticatedAndResourceBoundState extends State { + private AuthenticatedAndResourceBoundState(StateDescriptor stateDescriptor) { + super(stateDescriptor); + } + + @Override + protected TransitionIntoResult transitionInto(WalkStateGraphContext walkStateGraphContext) + throws NotConnectedException, InterruptedException { + if (walkFromDisconnectToAuthenticated != null) { + // If there was already a previous walk to ConnectedButUnauthenticated, then the context of the current + // walk must not start from the 'Disconnected' state. + assert (walkStateGraphContext.walkedStateGraphPath.get(0).stateDescriptor.getClass() != DisconnectedStateDescriptor.class); + walkFromDisconnectToAuthenticated.addAll(walkStateGraphContext.walkedStateGraphPath); + } else { + walkFromDisconnectToAuthenticated = new ArrayList<>(walkStateGraphContext.walkedStateGraphPath.size() + 1); + walkFromDisconnectToAuthenticated.addAll(walkStateGraphContext.walkedStateGraphPath); + } + walkFromDisconnectToAuthenticated.add(this); + + afterSuccessfulLogin(streamResumed); + return TransitionSuccessResult.EMPTY_INSTANCE; + } + + @Override + protected void resetState() { + authenticated = false; + } + } + + public void addConnectionStateMachineListener(ConnectionStateMachineListener connectionStateMachineListener) { + connectionStateMachineListeners.add(connectionStateMachineListener); + } + + public boolean removeConnectionStateMachineListener(ConnectionStateMachineListener connectionStateMachineListener) { + return connectionStateMachineListeners.remove(connectionStateMachineListener); + } + + protected void invokeConnectionStateMachineListener(ConnectionStateEvent connectionStateEvent) { + if (connectionStateMachineListeners.isEmpty()) { + return; + } + + ASYNC_BUT_ORDERED.performAsyncButOrdered(this, () -> { + for (ConnectionStateMachineListener connectionStateMachineListener : connectionStateMachineListeners) { + connectionStateMachineListener.onConnectionStateEvent(connectionStateEvent, this); + } + }); + } +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/fsm/ConnectionStateEvent.java b/smack-core/src/main/java/org/jivesoftware/smack/fsm/ConnectionStateEvent.java new file mode 100644 index 000000000..01e0faf22 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/fsm/ConnectionStateEvent.java @@ -0,0 +1,113 @@ +/** + * + * Copyright 2018 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.fsm; + +import org.jivesoftware.smack.fsm.AbstractXmppStateMachineConnection.State; +import org.jivesoftware.smack.fsm.AbstractXmppStateMachineConnection.TransitionFailureResult; +import org.jivesoftware.smack.fsm.AbstractXmppStateMachineConnection.TransitionImpossibleReason; +import org.jivesoftware.smack.fsm.AbstractXmppStateMachineConnection.TransitionSuccessResult; + +public class ConnectionStateEvent { + + private final StateDescriptor stateDescriptor; + + private final long timestamp; + + protected ConnectionStateEvent(StateDescriptor stateDescriptor) { + this.stateDescriptor = stateDescriptor; + this.timestamp = System.currentTimeMillis(); + } + + public StateDescriptor getStateDescriptor() { + return stateDescriptor; + } + + @Override + public String toString() { + return stateDescriptor.getStateName() + ' ' + getClass().getSimpleName(); + } + + public long getTimestamp() { + return timestamp; + } + + public static class StateRevertBackwardsWalk extends ConnectionStateEvent { + StateRevertBackwardsWalk(State state) { + super(state.getStateDescriptor()); + } + } + + public static class FinalStateReached extends ConnectionStateEvent { + FinalStateReached(State state) { + super(state.getStateDescriptor()); + } + } + + public static class TransitionNotPossible extends ConnectionStateEvent { + private final TransitionImpossibleReason transitionImpossibleReason; + + TransitionNotPossible(State state, TransitionImpossibleReason reason) { + super(state.getStateDescriptor()); + this.transitionImpossibleReason = reason; + } + + @Override + public String toString() { + return super.toString() + ": " + transitionImpossibleReason; + } + } + + public static class AboutToTransitionInto extends ConnectionStateEvent { + AboutToTransitionInto(State state) { + super(state.getStateDescriptor()); + } + } + + public static class TransitionFailed extends ConnectionStateEvent { + private final TransitionFailureResult transitionFailedReason; + + TransitionFailed(State state, TransitionFailureResult transitionFailedReason) { + super(state.getStateDescriptor()); + this.transitionFailedReason = transitionFailedReason; + } + + @Override + public String toString() { + return super.toString() + ": " + transitionFailedReason; + } + } + + public static class SuccessfullyTransitionedInto extends ConnectionStateEvent { + private final TransitionSuccessResult transitionSuccessResult; + + SuccessfullyTransitionedInto(State state, TransitionSuccessResult transitionSuccessResult) { + super(state.getStateDescriptor()); + this.transitionSuccessResult = transitionSuccessResult; + } + + @Override + public String toString() { + return super.toString() + ": " + transitionSuccessResult; + } + } + + public abstract static class DetailedTransitionIntoInformation extends ConnectionStateEvent { + protected DetailedTransitionIntoInformation(State state) { + super(state.getStateDescriptor()); + } + } +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/fsm/ConnectionStateMachineListener.java b/smack-core/src/main/java/org/jivesoftware/smack/fsm/ConnectionStateMachineListener.java new file mode 100644 index 000000000..e8dcb02b1 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/fsm/ConnectionStateMachineListener.java @@ -0,0 +1,24 @@ +/** + * + * Copyright 2018 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.fsm; + +// TODO: Mark as java.lang.FunctionalInterface once Smack's minimum Android API level is 24 or higher. +public interface ConnectionStateMachineListener { + + void onConnectionStateEvent(ConnectionStateEvent connectionStateEvent, AbstractXmppStateMachineConnection connection); + +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/fsm/LoginContext.java b/smack-core/src/main/java/org/jivesoftware/smack/fsm/LoginContext.java new file mode 100644 index 000000000..e0ac5ea14 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/fsm/LoginContext.java @@ -0,0 +1,33 @@ +/** + * + * Copyright 2018 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.fsm; + +import org.jxmpp.jid.parts.Resourcepart; + +// TODO: At one point SASL authzid should be part of this. +public class LoginContext { + final String username; + final String password; + final Resourcepart resource; + + LoginContext(String username, String password, Resourcepart resource) { + this.username = username; + this.password = password; + this.resource = resource; + } + +} 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 new file mode 100644 index 000000000..261529f25 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/fsm/StateDescriptor.java @@ -0,0 +1,221 @@ +/** + * + * Copyright 2018 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.fsm; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.logging.Logger; + +import org.jivesoftware.smack.fsm.AbstractXmppStateMachineConnection.State; + +public abstract class StateDescriptor { + + public enum Property { + multiVisitState, + finalState, + notImplemented, + } + + private static final Logger LOGGER = Logger.getLogger(StateDescriptor.class.getName()); + + private final String stateName; + private final int xepNum; + private final String rfcSection; + private final Set properties; + + private final Class stateClass; + private final Constructor stateClassConstructor; + + private final Set> successors = new HashSet<>(); + + private final Set> predecessors = new HashSet<>(); + + private final Set> precedenceOver = new HashSet<>(); + + private final Set> inferiorTo = new HashSet<>(); + + protected StateDescriptor() { + this(AbstractXmppStateMachineConnection.NoOpState.class, (Property) null); + } + + protected StateDescriptor(Property... properties) { + this(AbstractXmppStateMachineConnection.NoOpState.class, properties); + } + + protected StateDescriptor(Class stateClass) { + this(stateClass, -1, null, Collections.emptySet()); + } + + protected StateDescriptor(Class stateClass, Property... properties) { + this(stateClass, -1, null, new HashSet<>(Arrays.asList(properties))); + } + + protected StateDescriptor(Class stateClass, int xepNum) { + this(stateClass, xepNum, null, Collections.emptySet()); + } + + protected StateDescriptor(Class stateClass, int xepNum, + Property... properties) { + this(stateClass, xepNum, null, new HashSet<>(Arrays.asList(properties))); + } + + protected StateDescriptor(Class stateClass, String rfcSection) { + this(stateClass, -1, rfcSection, Collections.emptySet()); + } + + @SuppressWarnings("unchecked") + private StateDescriptor(Class stateClass, int xepNum, + String rfcSection, Set properties) { + this.stateClass = stateClass; + if (rfcSection != null && xepNum > 0) { + throw new IllegalArgumentException("Must specify either RFC or XEP"); + } + this.xepNum = xepNum; + this.rfcSection = rfcSection; + this.properties = properties; + + Constructor selectedConstructor = null; + Constructor[] constructors = stateClass.getDeclaredConstructors(); + for (Constructor constructor : constructors) { + Class[] parameterTypes = constructor.getParameterTypes(); + if (parameterTypes.length != 2) { + LOGGER.warning("Invalid State class constructor: " + constructor); + continue; + } + if (!AbstractXmppStateMachineConnection.class.isAssignableFrom(parameterTypes[0])) { + continue; + } + selectedConstructor = (Constructor) constructor; + break; + } + + if (selectedConstructor == null) { + throw new IllegalArgumentException(); + } + stateClassConstructor = selectedConstructor; + stateClassConstructor.setAccessible(true); + + String className = getClass().getSimpleName(); + stateName = className.replaceFirst("StateDescriptor", ""); + } + + protected void addSuccessor(Class successor) { + addAndCheckNonExistent(successors, successor); + } + + protected void addPredeccessor(Class predeccessor) { + addAndCheckNonExistent(predecessors, predeccessor); + } + + protected void declarePrecedenceOver(Class subordinate) { + addAndCheckNonExistent(precedenceOver, subordinate); + } + + protected void declareInferiortyTo(Class superior) { + addAndCheckNonExistent(inferiorTo, superior); + } + + private static void addAndCheckNonExistent(Set set, E e) { + boolean newElement = set.add(e); + if (!newElement) { + throw new IllegalArgumentException("Element already exists in set"); + } + } + + public Set> getSuccessors() { + return Collections.unmodifiableSet(successors); + } + + public Set> getPredeccessors() { + return Collections.unmodifiableSet(predecessors); + } + + public Set> getSubordinates() { + return Collections.unmodifiableSet(precedenceOver); + } + + public Set> getSuperiors() { + return Collections.unmodifiableSet(inferiorTo); + } + + public String getStateName() { + return stateName; + } + + public String getFullStateName(boolean breakStateName) { + String reference = getReference(); + if (reference != null) { + char sep; + if (breakStateName) { + sep = '\n'; + } else { + sep = ' '; + } + return getStateName() + sep + '(' + reference + ')'; + } + else { + return getStateName(); + } + } + + private transient String referenceCache; + + public String getReference() { + if (referenceCache == null) { + if (xepNum > 0) { + referenceCache = "XEP-" + String.format("%04d", xepNum); + } else if (rfcSection != null) { + referenceCache = rfcSection; + } + } + return referenceCache; + } + + public Class getStateClass() { + return stateClass; + } + + public boolean isMultiVisitState() { + return properties.contains(Property.multiVisitState); + } + + public boolean isNotImplemented() { + return properties.contains(Property.notImplemented); + } + + public boolean isFinalState() { + return properties.contains(Property.finalState); + } + + protected final AbstractXmppStateMachineConnection.State constructState(AbstractXmppStateMachineConnection connection) { + try { + return stateClassConstructor.newInstance(connection, this); + } catch (InstantiationException | IllegalAccessException | IllegalArgumentException + | InvocationTargetException e) { + throw new IllegalStateException(e); + } + } + + @Override + public String toString() { + return "StateDescriptor " + stateName; + } +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/fsm/StateDescriptorGraph.java b/smack-core/src/main/java/org/jivesoftware/smack/fsm/StateDescriptorGraph.java new file mode 100644 index 000000000..104adbb8c --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/fsm/StateDescriptorGraph.java @@ -0,0 +1,419 @@ +/** + * + * Copyright 2018 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.fsm; + +import java.io.PrintWriter; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.logging.Logger; + +import org.jivesoftware.smack.fsm.AbstractXmppStateMachineConnection.DisconnectedStateDescriptor; +import org.jivesoftware.smack.util.MultiMap; + +/** + * Smack's utility API for Finite State Machines (FSM). + * + *

+ * Thanks to Andreas Fried for the fun and successful bug hunting session. + *

+ * + * @author Florian Schmaus + * + */ +public class StateDescriptorGraph { + + private static final Logger LOGGER = Logger.getLogger(StateDescriptorGraph.class.getName()); + + private static GraphVertex addNewStateDescriptorGraphVertex( + Class stateDescriptorClass, + Map, GraphVertex> graphVertexes) + throws InstantiationException, IllegalAccessException, IllegalArgumentException, + InvocationTargetException, NoSuchMethodException, SecurityException { + Constructor stateDescriptorConstructor = stateDescriptorClass.getDeclaredConstructor(); + stateDescriptorConstructor.setAccessible(true); + StateDescriptor stateDescriptor = stateDescriptorConstructor.newInstance(); + GraphVertex graphVertexStateDescriptor = new GraphVertex<>(stateDescriptor); + + GraphVertex previous = graphVertexes.put(stateDescriptorClass, graphVertexStateDescriptor); + assert previous == null; + + return graphVertexStateDescriptor; + } + + private static final class HandleStateDescriptorGraphVertexContext { + private final Set> handledStateDescriptors = new HashSet<>(); + Map, GraphVertex> graphVertexes; + MultiMap, Class> inferredForwardEdges; + + private HandleStateDescriptorGraphVertexContext( + Map, GraphVertex> graphVertexes, + MultiMap, Class> inferredForwardEdges) { + this.graphVertexes = graphVertexes; + this.inferredForwardEdges = inferredForwardEdges; + } + + private boolean recurseInto(Class stateDescriptorClass) { + boolean wasAdded = handledStateDescriptors.add(stateDescriptorClass); + boolean alreadyHandled = !wasAdded; + return alreadyHandled; + } + + private GraphVertex getOrConstruct(Class stateDescriptorClass) + throws InstantiationException, IllegalAccessException, IllegalArgumentException, + InvocationTargetException, NoSuchMethodException, SecurityException { + GraphVertex graphVertexStateDescriptor = graphVertexes.get(stateDescriptorClass); + + if (graphVertexStateDescriptor == null) { + graphVertexStateDescriptor = addNewStateDescriptorGraphVertex(stateDescriptorClass, graphVertexes); + + for (Class inferredSuccessor : inferredForwardEdges.getAll( + stateDescriptorClass)) { + graphVertexStateDescriptor.getElement().addSuccessor(inferredSuccessor); + } + } + + return graphVertexStateDescriptor; + } + } + + private static void handleStateDescriptorGraphVertex(GraphVertex node, + HandleStateDescriptorGraphVertexContext context) + throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException { + Class stateDescriptorClass = node.element.getClass(); + boolean alreadyHandled = context.recurseInto(stateDescriptorClass); + if (alreadyHandled) { + return; + } + + Set> successorClasses = node.element.getSuccessors(); + int numSuccessors = successorClasses.size(); + + Map, GraphVertex> successorStateDescriptors = new HashMap<>( + numSuccessors); + for (Class successorClass : successorClasses) { + GraphVertex successorGraphNode = context.getOrConstruct(successorClass); + successorStateDescriptors.put(successorClass, successorGraphNode); + } + + switch (numSuccessors) { + case 0: + throw new IllegalStateException("State " + stateDescriptorClass + " has no successor"); + case 1: + GraphVertex soleSuccessorNode = successorStateDescriptors.values().iterator().next(); + node.addOutgoingEdge(soleSuccessorNode); + handleStateDescriptorGraphVertex(soleSuccessorNode, context); + return; + } + + // We hit a state with multiple successors, perform a topological sort on the successors first. + // Process the information regarding subordinates and superiors states. + + // The preference graph is the graph where the precedence information of all successors is stored, which we will + // topologically sort to find out which successor we should try first. It is a further new graph we use solely in + // this step for every node. The graph is representent as map. There is no special marker for the initial node + // as it is not required for the topological sort performed later. + Map, GraphVertex>> preferenceGraph = new HashMap<>(numSuccessors); + + // Iterate over all successor states of the current state. + for (GraphVertex successorStateDescriptorGraphNode : successorStateDescriptors.values()) { + StateDescriptor successorStateDescriptor = successorStateDescriptorGraphNode.element; + Class successorStateDescriptorClass = successorStateDescriptor.getClass(); + for (Class subordinateClass : successorStateDescriptor.getSubordinates()) { + if (!successorClasses.contains(subordinateClass)) { + LOGGER.severe(successorStateDescriptor + " points to a subordinate '" + subordinateClass + "' which is not part of the successor set"); + continue; + } + + GraphVertex> superiorClassNode = lookupAndCreateIfRequired( + preferenceGraph, successorStateDescriptorClass); + GraphVertex> subordinateClassNode = lookupAndCreateIfRequired( + preferenceGraph, subordinateClass); + + superiorClassNode.addOutgoingEdge(subordinateClassNode); + } + for (Class superiorClass : successorStateDescriptor.getSuperiors()) { + if (!successorClasses.contains(superiorClass)) { + LOGGER.severe(successorStateDescriptor + " points to a superior '" + superiorClass + + "' which is not part of the successor set"); + continue; + } + + GraphVertex> subordinateClassNode = lookupAndCreateIfRequired( + preferenceGraph, successorStateDescriptorClass); + GraphVertex> superiorClassNode = lookupAndCreateIfRequired( + preferenceGraph, superiorClass); + + superiorClassNode.addOutgoingEdge(subordinateClassNode); + } + } + + // Perform a topological sort which returns the state descriptor classes in their priority. + List>> sortedSuccessors = topologicalSort(preferenceGraph.values()); + + // Handle the successor nodes which have not preference information available. Simply append them to the end of + // the sorted successor list. + outerloop: for (Class successorStateDescriptor : successorClasses) { + for (GraphVertex> sortedSuccessor : sortedSuccessors) { + if (sortedSuccessor.getElement() == successorStateDescriptor) { + continue outerloop; + } + } + + sortedSuccessors.add(new GraphVertex<>(successorStateDescriptor)); + } + + for (GraphVertex> successor : sortedSuccessors) { + GraphVertex successorVertex = successorStateDescriptors.get(successor.element); + node.addOutgoingEdge(successorVertex); + + // Recurse further. + handleStateDescriptorGraphVertex(successorVertex, context); + } + } + + public static GraphVertex constructStateDescriptorGraph(Set> backwardEdgeStateDescriptors) + throws InstantiationException, IllegalAccessException, IllegalArgumentException, + InvocationTargetException, NoSuchMethodException, SecurityException { + Map, GraphVertex> graphVertexes = new HashMap<>(); + + final Class initialStatedescriptorClass = DisconnectedStateDescriptor.class; + GraphVertex initialNode = addNewStateDescriptorGraphVertex(initialStatedescriptorClass, graphVertexes); + + MultiMap, Class> inferredForwardEdges = new MultiMap<>(); + for (Class backwardsEdge : backwardEdgeStateDescriptors) { + GraphVertex graphVertexStateDescriptor = addNewStateDescriptorGraphVertex(backwardsEdge, graphVertexes); + + for (Class predecessor : graphVertexStateDescriptor.getElement().getPredeccessors()) { + inferredForwardEdges.put(predecessor, backwardsEdge); + } + } + // Ensure that the intial node has their successors inferred. + for (Class inferredSuccessorOfInitialStateDescriptor : inferredForwardEdges.getAll(initialStatedescriptorClass)) { + initialNode.getElement().addSuccessor(inferredSuccessorOfInitialStateDescriptor); + } + + HandleStateDescriptorGraphVertexContext context = new HandleStateDescriptorGraphVertexContext(graphVertexes, inferredForwardEdges); + handleStateDescriptorGraphVertex(initialNode, context); + + return initialNode; + } + + private static GraphVertex convertToStateGraph(GraphVertex stateDescriptorVertex, + AbstractXmppStateMachineConnection connection, Map> handledStateDescriptors) { + StateDescriptor stateDescriptor = stateDescriptorVertex.getElement(); + GraphVertex stateVertex = handledStateDescriptors.get(stateDescriptor); + if (stateVertex != null) { + return stateVertex; + } + + AbstractXmppStateMachineConnection.State state = stateDescriptor.constructState(connection); + stateVertex = new GraphVertex<>(state); + handledStateDescriptors.put(stateDescriptor, stateVertex); + for (GraphVertex successorStateDescriptorVertex : stateDescriptorVertex.getOutgoingEdges()) { + GraphVertex successorStateVertex = convertToStateGraph(successorStateDescriptorVertex, connection, handledStateDescriptors); + // It is important that we keep the order of the edges. This should do it. + stateVertex.addOutgoingEdge(successorStateVertex); + } + + return stateVertex; + } + + static GraphVertex convertToStateGraph(GraphVertex initialStateDescriptor, + AbstractXmppStateMachineConnection connection) { + Map> handledStateDescriptors = new HashMap<>(); + GraphVertex initialState = convertToStateGraph(initialStateDescriptor, connection, + handledStateDescriptors); + return initialState; + } + + // Graph API after here. + // This API could possibly factored out into an extra package/class, but then we will probably need a builder for + // the graph vertex in order to keep it immutable. + public static final class GraphVertex { + private final E element; + private final List> outgoingEdges = new ArrayList<>(); + + private VertexColor color = VertexColor.white; + + private GraphVertex(E element) { + this.element = element; + } + + private void addOutgoingEdge(GraphVertex vertex) { + assert vertex != null; + if (outgoingEdges.contains(vertex)) { + throw new IllegalArgumentException("This " + this + " already has an outgoing edge to " + vertex); + } + outgoingEdges.add(vertex); + } + + public E getElement() { + return element; + } + + public List> getOutgoingEdges() { + return Collections.unmodifiableList(outgoingEdges); + } + + private enum VertexColor { + white, + grey, + black, + } + + @Override + public String toString() { + return toString(true); + } + + public String toString(boolean includeOutgoingEdges) { + StringBuilder sb = new StringBuilder(); + sb.append("GraphVertex " + element + " [color=" + color + + ", identityHashCode=" + System.identityHashCode(this) + + ", outgoingEdgeCount=" + outgoingEdges.size()); + + if (includeOutgoingEdges) { + sb.append(", outgoingEdges={"); + + for (Iterator> it = outgoingEdges.iterator(); it.hasNext();) { + GraphVertex outgoingEdgeVertex = it.next(); + sb.append(outgoingEdgeVertex.toString(false)); + if (it.hasNext()) { + sb.append(", "); + } + } + sb.append('}'); + } + + sb.append(']'); + return sb.toString(); + } + } + + private static GraphVertex> lookupAndCreateIfRequired( + Map, GraphVertex>> map, + Class clazz) { + GraphVertex> vertex = map.get(clazz); + if (vertex == null) { + vertex = new GraphVertex<>(clazz); + map.put(clazz, vertex); + } + return vertex; + } + + private static List> topologicalSort(Collection> vertexes) { + List> res = new ArrayList<>(); + dfs(vertexes, (vertex) -> res.add(0, vertex), null); + return res; + } + + private static void dfsVisit(GraphVertex vertex, DfsFinishedVertex dfsFinishedVertex, DfsEdgeFound dfsEdgeFound) { + vertex.color = GraphVertex.VertexColor.grey; + + final int totalEdgeCount = vertex.getOutgoingEdges().size(); + + int edgeCount = 0; + + for (GraphVertex successorVertex : vertex.getOutgoingEdges()) { + edgeCount++; + if (dfsEdgeFound != null) { + dfsEdgeFound.onEdgeFound(vertex, successorVertex, edgeCount, totalEdgeCount); + } + if (successorVertex.color == GraphVertex.VertexColor.white) { + dfsVisit(successorVertex, dfsFinishedVertex, dfsEdgeFound); + } + } + + vertex.color = GraphVertex.VertexColor.black; + if (dfsFinishedVertex != null) { + dfsFinishedVertex.onVertexFinished(vertex); + } + } + + private static void dfs(Collection> vertexes, DfsFinishedVertex dfsFinishedVertex, DfsEdgeFound dfsEdgeFound) { + for (GraphVertex vertex : vertexes) { + if (vertex.color == GraphVertex.VertexColor.white) { + dfsVisit(vertex, dfsFinishedVertex, dfsEdgeFound); + } + } + } + + public static void stateDescriptorGraphToDot(Collection> vertexes, + PrintWriter dotOut, boolean breakStateName) { + dotOut.append("digraph {\n"); + dfs(vertexes, + (finishedVertex) -> { + boolean isMultiVisitState = finishedVertex.element.isMultiVisitState(); + boolean isFinalState = finishedVertex.element.isFinalState(); + boolean isNotImplemented = finishedVertex.element.isNotImplemented(); + + String style = null; + if (isMultiVisitState) { + style = "bold"; + } else if (isFinalState) { + style = "filled"; + } else if (isNotImplemented) { + style = "dashed"; + } + + if (style == null) { + return; + } + + dotOut.append('"') + .append(finishedVertex.element.getFullStateName(breakStateName)) + .append("\" [ ") + .append("style=") + .append(style) + .append(" ]\n"); + }, + (from, to, edgeId, totalEdgeCount) -> { + dotOut.append(" \"") + .append(from.element.getFullStateName(breakStateName)) + .append("\" -> \"") + .append(to.element.getFullStateName(breakStateName)) + .append('"'); + if (totalEdgeCount > 1) { + // Note that 'dot' requires *double* quotes to enclose the value. + dotOut.append(" [xlabel=\"") + .append(Integer.toString(edgeId)) + .append("\"]"); + } + dotOut.append(";\n"); + }); + dotOut.append("}\n"); + } + + // TODO: Replace with java.util.function.Consumer> once Smack's minimum Android SDK level is 24 or higher. + private interface DfsFinishedVertex { + void onVertexFinished(GraphVertex vertex); + } + + // TODO: Replace with java.util.function.Consumer> once Smack's minimum Android SDK level is 24 or higher. + private interface DfsEdgeFound { + void onEdgeFound(GraphVertex from, GraphVertex to, int edgeId, int totalEdgeCount); + } +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/fsm/StateMachineException.java b/smack-core/src/main/java/org/jivesoftware/smack/fsm/StateMachineException.java new file mode 100644 index 000000000..ed0f37fe1 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/fsm/StateMachineException.java @@ -0,0 +1,60 @@ +/** + * + * Copyright 2018-2019 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.fsm; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.fsm.AbstractXmppStateMachineConnection.State; +import org.jivesoftware.smack.fsm.AbstractXmppStateMachineConnection.TransitionReason; + +public abstract class StateMachineException extends SmackException { + + private static final long serialVersionUID = 1L; + + public static class SmackMandatoryStateFailedException extends StateMachineException { + + private static final long serialVersionUID = 1L; + + SmackMandatoryStateFailedException(State state, TransitionReason failureReason) { + } + } + + public static final class SmackStateGraphDeadEndException extends StateMachineException { + + private final List walkedStateGraphPath; + + private final Map failedStates; + + private static final long serialVersionUID = 1L; + + SmackStateGraphDeadEndException(List walkedStateGraphPath, Map failedStates) { + this.walkedStateGraphPath = Collections.unmodifiableList(walkedStateGraphPath); + this.failedStates = Collections.unmodifiableMap(failedStates); + } + + public List getWalkedStateGraph() { + return walkedStateGraphPath; + } + + public Map getFailedStates() { + return failedStates; + } + } +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/fsm/package-info.java b/smack-core/src/main/java/org/jivesoftware/smack/fsm/package-info.java new file mode 100644 index 000000000..b944fd65a --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/fsm/package-info.java @@ -0,0 +1,21 @@ +/** + * + * Copyright 2018 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Smack's Finite State Machine to handle the login logic. + * + */ +package org.jivesoftware.smack.fsm; diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/ExtensionElement.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/ExtensionElement.java index 783eb5b7d..86dfe5291 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/packet/ExtensionElement.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/ExtensionElement.java @@ -1,6 +1,6 @@ /** * - * Copyright 2003-2007 Jive Software. + * Copyright 2003-2007 Jive Software, 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. @@ -14,7 +14,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.jivesoftware.smack.packet; /** @@ -33,13 +32,6 @@ package org.jivesoftware.smack.packet; * @see org.jivesoftware.smack.provider.ExtensionElementProvider * @author Matt Tucker */ -public interface ExtensionElement extends NamedElement { - - /** - * Returns the root element XML namespace. - * - * @return the namespace. - */ - String getNamespace(); +public interface ExtensionElement extends FullyQualifiedElement { } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/FullyQualifiedElement.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/FullyQualifiedElement.java new file mode 100644 index 000000000..37e1b425a --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/FullyQualifiedElement.java @@ -0,0 +1,28 @@ +/** + * + * Copyright 2018 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.packet; + +public interface FullyQualifiedElement extends NamedElement { + + /** + * Returns the root element XML namespace. + * + * @return the namespace. + */ + String getNamespace(); + +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/Nonza.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/Nonza.java index fb8a1e94c..6e2435637 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/packet/Nonza.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/Nonza.java @@ -30,6 +30,6 @@ package org.jivesoftware.smack.packet; * @author Florian Schmaus * @see XEP-0360: Nonzas (are not Stanzas) */ -public interface Nonza extends TopLevelStreamElement, ExtensionElement { +public interface Nonza extends TopLevelStreamElement, FullyQualifiedElement { } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/StartTls.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/StartTls.java index 7efbda799..bea488ad1 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/packet/StartTls.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/StartTls.java @@ -1,6 +1,6 @@ /** * - * Copyright ยฉ 2014 Florian Schmaus + * Copyright ยฉ 2014-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. @@ -20,6 +20,8 @@ import org.jivesoftware.smack.util.XmlStringBuilder; public class StartTls implements Nonza { + public static final StartTls INSTANCE = new StartTls(); + public static final String ELEMENT = "starttls"; public static final String NAMESPACE = "urn:ietf:params:xml:ns:xmpp-tls"; @@ -49,7 +51,7 @@ public class StartTls implements Nonza { @Override public XmlStringBuilder toXML(String enclosingNamespace) { - XmlStringBuilder xml = new XmlStringBuilder(this); + XmlStringBuilder xml = new XmlStringBuilder(this, enclosingNamespace); xml.rightAngleBracket(); xml.condEmptyElement(required, "required"); xml.closeElement(this); diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/StreamClose.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/StreamClose.java new file mode 100644 index 000000000..bba37b6fe --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/StreamClose.java @@ -0,0 +1,42 @@ +/** + * + * Copyright 2018 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.packet; + +public final class StreamClose implements Nonza { + + public static final StreamClose INSTANCE = new StreamClose(); + + private StreamClose() { + } + + @Override + public String toXML(String enclosingNamespace) { + return "'; + } + + @Override + public String getNamespace() { + // Closing XML tags do never explicitly state their namespace. + return "(none)"; + } + + @Override + public String getElementName() { + return StreamOpen.ELEMENT; + } + +} 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 new file mode 100644 index 000000000..d64ef1984 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/TlsFailure.java @@ -0,0 +1,43 @@ +/** + * + * Copyright 2018 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.packet; + +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; + + private TlsFailure() { + } + + @Override + public String getElementName() { + return ELEMENT; + } + + @Override + public String getNamespace() { + return NAMESPACE; + } + + @Override + public String toXML(String enclosingNamespace) { + return '<' + ELEMENT + " xmlns='" + NAMESPACE + "'/>"; + } +} 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 new file mode 100644 index 000000000..19d80671b --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/TlsProceed.java @@ -0,0 +1,43 @@ +/** + * + * Copyright 2018 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.packet; + +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"; + + private TlsProceed() { + } + + @Override + public String getElementName() { + return ELEMENT; + } + + @Override + public String getNamespace() { + return NAMESPACE; + } + + @Override + public String toXML(String enclosingNamespace) { + return '<' + ELEMENT + " xmlns='" + NAMESPACE + "'/>"; + } +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/TopLevelStreamElement.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/TopLevelStreamElement.java index e5ada91e1..b952a5d54 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/packet/TopLevelStreamElement.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/TopLevelStreamElement.java @@ -1,6 +1,6 @@ /** * - * Copyright ยฉ 2014 Florian Schmaus + * Copyright ยฉ 2014-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. @@ -14,7 +14,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.jivesoftware.smack.packet; /** diff --git a/smack-core/src/main/java/org/jivesoftware/smack/provider/NonzaProvider.java b/smack-core/src/main/java/org/jivesoftware/smack/provider/NonzaProvider.java new file mode 100644 index 000000000..45923c7f3 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/provider/NonzaProvider.java @@ -0,0 +1,24 @@ +/** + * + * Copyright 2018 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jivesoftware.smack.provider; + +import org.jivesoftware.smack.packet.Nonza; + +public abstract class NonzaProvider extends Provider { + +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/provider/Provider.java b/smack-core/src/main/java/org/jivesoftware/smack/provider/Provider.java index 082855463..35983cb86 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/provider/Provider.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/provider/Provider.java @@ -17,6 +17,9 @@ package org.jivesoftware.smack.provider; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + import org.jivesoftware.smack.packet.Element; import org.jivesoftware.smack.util.ParserUtils; @@ -35,6 +38,26 @@ import org.xmlpull.v1.XmlPullParser; */ public abstract class Provider { + private final Class elementClass; + + @SuppressWarnings("unchecked") + protected Provider() { + Type currentType = getClass().getGenericSuperclass(); + while (!(currentType instanceof ParameterizedType)) { + Class currentClass = (Class) currentType; + currentType = currentClass.getGenericSuperclass(); + } + ParameterizedType parameterizedGenericSuperclass = (ParameterizedType) currentType; + Type[] actualTypeArguments = parameterizedGenericSuperclass.getActualTypeArguments(); + Type elementType = actualTypeArguments[0]; + + elementClass = (Class) elementType; + } + + public final Class getElementClass() { + return elementClass; + } + public final E parse(XmlPullParser parser) throws Exception { // XPP3 calling convention assert: Parser should be at start tag ParserUtils.assertAtStartTag(parser); diff --git a/smack-core/src/main/java/org/jivesoftware/smack/provider/ProviderManager.java b/smack-core/src/main/java/org/jivesoftware/smack/provider/ProviderManager.java index 0e581bc7d..b2da1392f 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/provider/ProviderManager.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/provider/ProviderManager.java @@ -25,7 +25,9 @@ import java.util.concurrent.ConcurrentHashMap; import org.jivesoftware.smack.SmackConfiguration; import org.jivesoftware.smack.packet.ExtensionElement; import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.Nonza; import org.jivesoftware.smack.util.StringUtils; +import org.jivesoftware.smack.util.XmppElementUtil; import org.jxmpp.util.XmppStringUtils; @@ -113,6 +115,7 @@ public final class ProviderManager { private static final Map> extensionProviders = new ConcurrentHashMap>(); private static final Map> iqProviders = new ConcurrentHashMap>(); private static final Map> streamFeatureProviders = new ConcurrentHashMap>(); + private static final Map> nonzaProviders = new ConcurrentHashMap<>(); static { // Ensure that Smack is initialized by calling getVersion, so that user @@ -309,6 +312,31 @@ public final class ProviderManager { streamFeatureProviders.remove(key); } + public static NonzaProvider getNonzaProvider(String elementName, String namespace) { + String key = getKey(elementName, namespace); + return getNonzaProvider(key); + } + + public static NonzaProvider getNonzaProvider(String key) { + return nonzaProviders.get(key); + } + + public static void addNonzaProvider(NonzaProvider nonzaProvider) { + Class nonzaClass = nonzaProvider.getElementClass(); + String key = XmppElementUtil.getKeyFor(nonzaClass); + nonzaProviders.put(key, nonzaProvider); + } + + public static void removeNonzaProvider(Class nonzaClass) { + String key = XmppElementUtil.getKeyFor(nonzaClass); + nonzaProviders.remove(key); + } + + public static void removeNonzaProvider(String elementName, String namespace) { + String key = getKey(elementName, namespace); + nonzaProviders.remove(key); + } + private static String getKey(String elementName, String namespace) { return XmppStringUtils.generateKey(elementName, namespace); } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/provider/TlsFailureProvider.java b/smack-core/src/main/java/org/jivesoftware/smack/provider/TlsFailureProvider.java new file mode 100644 index 000000000..c568ccc88 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/provider/TlsFailureProvider.java @@ -0,0 +1,35 @@ +/** + * + * Copyright 2018 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.provider; + +import org.jivesoftware.smack.packet.TlsProceed; + +import org.xmlpull.v1.XmlPullParser; + +public final class TlsFailureProvider extends NonzaProvider { + + public static final TlsFailureProvider INSTANCE = new TlsFailureProvider(); + + private TlsFailureProvider() { + } + + @Override + public TlsProceed parse(XmlPullParser parser, int initialDepth) throws Exception { + return TlsProceed.INSTANCE; + } + +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/provider/TlsProceedProvider.java b/smack-core/src/main/java/org/jivesoftware/smack/provider/TlsProceedProvider.java new file mode 100644 index 000000000..917487bdf --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/provider/TlsProceedProvider.java @@ -0,0 +1,35 @@ +/** + * + * Copyright 2018 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.provider; + +import org.jivesoftware.smack.packet.TlsFailure; + +import org.xmlpull.v1.XmlPullParser; + +public final class TlsProceedProvider extends NonzaProvider { + + public static final TlsProceedProvider INSTANCE = new TlsProceedProvider(); + + private TlsProceedProvider() { + } + + @Override + public TlsFailure parse(XmlPullParser parser, int initialDepth) throws Exception { + return TlsFailure.INSTANCE; + } + +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/ArrayBlockingQueueWithShutdown.java b/smack-core/src/main/java/org/jivesoftware/smack/util/ArrayBlockingQueueWithShutdown.java index ab95510df..1288b8f18 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/ArrayBlockingQueueWithShutdown.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/ArrayBlockingQueueWithShutdown.java @@ -232,6 +232,19 @@ public class ArrayBlockingQueueWithShutdown extends AbstractQueue implemen } } + public boolean offerAndShutdown(E e) { + checkNotNull(e); + boolean res; + lock.lock(); + try { + res = offer(e); + shutdown(); + } finally { + lock.unlock(); + } + return res; + } + private void putInternal(E e, boolean signalNotEmpty) throws InterruptedException { assert lock.isHeldByCurrentThread(); diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/Async.java b/smack-core/src/main/java/org/jivesoftware/smack/util/Async.java index 2ffe77e5f..3086d94bd 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/Async.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/Async.java @@ -1,6 +1,6 @@ /** * - * Copyright 2014 Florian Schmaus + * Copyright 2014-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. diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/CollectionUtil.java b/smack-core/src/main/java/org/jivesoftware/smack/util/CollectionUtil.java index 6b3aa9d87..6da004405 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/CollectionUtil.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/CollectionUtil.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015 Florian Schmaus + * Copyright 2015-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,7 +16,10 @@ */ package org.jivesoftware.smack.util; +import java.util.ArrayList; import java.util.Collection; +import java.util.Iterator; +import java.util.List; public class CollectionUtil { @@ -30,4 +33,20 @@ public class CollectionUtil { return collection; } + public static > List removeUntil(C collection, Predicate predicate) { + List removedElements = new ArrayList<>(collection.size()); + for (Iterator it = collection.iterator(); it.hasNext();) { + T t = it.next(); + if (predicate.test(t)) { + break; + } + removedElements.add(t); + it.remove(); + } + return removedElements; + } + + public interface Predicate { + boolean test(T t); + } } 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 1a6401e05..3f2c2ccbb 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 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. @@ -170,6 +170,33 @@ public class MultiMap { return res; } + /** + * Remove the given number of values for a given key. May return less values then requested. + * + * @param key the key to remove from. + * @param num the number of values to remove. + * @return a list of the removed values. + * @since 4.4.0 + */ + public List remove(K key, int num) { + List values = map.get(key); + if (values == null) { + return Collections.emptyList(); + } + + final int resultSize = values.size() > num ? num : values.size(); + final List result = new ArrayList<>(resultSize); + for (int i = 0; i < resultSize; i++) { + result.add(values.get(0)); + } + + if (values.isEmpty()) { + map.remove(key); + } + + return result; + } + public void putAll(Map map) { for (Map.Entry entry : map.entrySet()) { put(entry.getKey(), entry.getValue()); diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/PacketParserUtils.java b/smack-core/src/main/java/org/jivesoftware/smack/util/PacketParserUtils.java index 56d63a5e4..d1326a950 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/PacketParserUtils.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/PacketParserUtils.java @@ -96,6 +96,7 @@ public class PacketParserUtils { XML_PULL_PARSER_SUPPORTS_ROUNDTRIP = roundtrip; } + // TODO: Rename argument name from 'stanza' to 'element'. public static XmlPullParser getParserFor(String stanza) throws XmlPullParserException, IOException { return getParserFor(new StringReader(stanza)); } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/StringUtils.java b/smack-core/src/main/java/org/jivesoftware/smack/util/StringUtils.java index 31c87b4ee..affe6a047 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/StringUtils.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/StringUtils.java @@ -302,7 +302,7 @@ public class StringUtils { return randomString(length, SECURE_RANDOM.get()); } - private static String randomString(final int length, Random random) { + public static String randomString(final int length, Random random) { if (length < 1) { return null; } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/UTF8.java b/smack-core/src/main/java/org/jivesoftware/smack/util/UTF8.java new file mode 100644 index 000000000..55fc13454 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/UTF8.java @@ -0,0 +1,39 @@ +/** + * + * Copyright 2018 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.util; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; + +public class UTF8 { + + public static final String UTF8_CHARSET_NAME = "UTF-8"; + + private static final Charset utf8Charset; + + static { + utf8Charset = Charset.forName(UTF8_CHARSET_NAME); + } + + public static ByteBuffer encode(CharSequence charSequence) { + return encode(charSequence.toString()); + } + + public static ByteBuffer encode(String string) { + return utf8Charset.encode(string); + } +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/XmlStringBuilder.java b/smack-core/src/main/java/org/jivesoftware/smack/util/XmlStringBuilder.java index 2fa649540..c456557e3 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/XmlStringBuilder.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/XmlStringBuilder.java @@ -20,9 +20,11 @@ import java.io.IOException; import java.io.Writer; import java.util.Collection; import java.util.Date; +import java.util.Iterator; import org.jivesoftware.smack.packet.Element; import org.jivesoftware.smack.packet.ExtensionElement; +import org.jivesoftware.smack.packet.FullyQualifiedElement; import org.jivesoftware.smack.packet.NamedElement; import org.jxmpp.util.XmppDateTime; @@ -53,7 +55,7 @@ public class XmlStringBuilder implements Appendable, CharSequence, Element { halfOpenElement(e.getElementName()); } - public XmlStringBuilder(ExtensionElement ee, String enclosingNamespace) { + public XmlStringBuilder(FullyQualifiedElement ee, String enclosingNamespace) { this(enclosingNamespace); prelude(ee); } @@ -439,7 +441,7 @@ public class XmlStringBuilder implements Appendable, CharSequence, Element { return escape(text.toString()); } - public XmlStringBuilder prelude(ExtensionElement pe) { + public XmlStringBuilder prelude(FullyQualifiedElement pe) { return prelude(pe.getElementName(), pe.getNamespace()); } @@ -585,6 +587,10 @@ public class XmlStringBuilder implements Appendable, CharSequence, Element { } } + public Iterator getCharSequenceIterator() { + return sb.getAsList().iterator(); + } + @Override public CharSequence toXML(String enclosingNamespace) { StringBuilder res = new StringBuilder(); diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/XmppElementUtil.java b/smack-core/src/main/java/org/jivesoftware/smack/util/XmppElementUtil.java new file mode 100644 index 000000000..79729b662 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/XmppElementUtil.java @@ -0,0 +1,45 @@ +/** + * + * Copyright 2018 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.util; + +import org.jivesoftware.smack.packet.FullyQualifiedElement; + +import org.jxmpp.util.XmppStringUtils; + +public class XmppElementUtil { + + public static String getKeyFor(Class fullyQualifiedElement) { + String element, namespace; + try { + element = (String) fullyQualifiedElement.getField("ELEMENT").get(null); + namespace = (String) fullyQualifiedElement.getField("NAMESPACE").get(null); + } + catch (IllegalArgumentException | IllegalAccessException | NoSuchFieldException | SecurityException e) { + throw new IllegalArgumentException(e); + } + + String key = XmppStringUtils.generateKey(element, namespace); + return key; + } + + public static String getKeyFor(FullyQualifiedElement fullyQualifiedElement) { + String element = fullyQualifiedElement.getElementName(); + String namespace = fullyQualifiedElement.getNamespace(); + String key = XmppStringUtils.generateKey(element, namespace); + return key; + } +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/dns/HostAddress.java b/smack-core/src/main/java/org/jivesoftware/smack/util/dns/HostAddress.java index 9c988defd..686c23d37 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/dns/HostAddress.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/dns/HostAddress.java @@ -17,6 +17,7 @@ package org.jivesoftware.smack.util.dns; import java.net.InetAddress; +import java.net.InetSocketAddress; import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashMap; @@ -72,6 +73,14 @@ public class HostAddress { setException(e); } + public HostAddress(InetSocketAddress inetSocketAddress, Exception exception) { + String hostString = inetSocketAddress.getHostString(); + this.fqdn = DnsName.from(hostString); + this.port = inetSocketAddress.getPort(); + inetAddresses = Collections.emptyList(); + setException(exception); + } + public String getHost() { if (fqdn != null) { return fqdn.toString(); diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/dns/SmackDaneVerifier.java b/smack-core/src/main/java/org/jivesoftware/smack/util/dns/SmackDaneVerifier.java index 551cb8c59..98549e0bb 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/dns/SmackDaneVerifier.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/dns/SmackDaneVerifier.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015-2016 Florian Schmaus + * Copyright 2015-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. @@ -22,6 +22,7 @@ import java.security.cert.CertificateException; import javax.net.ssl.KeyManager; import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocket; import javax.net.ssl.X509TrustManager; @@ -31,5 +32,8 @@ import javax.net.ssl.X509TrustManager; public interface SmackDaneVerifier { void init(SSLContext context, KeyManager[] km, X509TrustManager tm, SecureRandom random) throws KeyManagementException; + // TODO: Remove this method in favor of finish(SSLSession). void finish(SSLSocket socket) throws CertificateException; + + void finish(SSLSession sslSession) throws CertificateException; } diff --git a/smack-core/src/test/java/org/jivesoftware/smack/DummyConnection.java b/smack-core/src/test/java/org/jivesoftware/smack/DummyConnection.java index a3ff3246f..bfe286823 100644 --- a/smack-core/src/test/java/org/jivesoftware/smack/DummyConnection.java +++ b/smack-core/src/test/java/org/jivesoftware/smack/DummyConnection.java @@ -55,7 +55,7 @@ public class DummyConnection extends AbstractXMPPConnection { private final BlockingQueue queue = new LinkedBlockingQueue(); - public static ConnectionConfiguration.Builder getDummyConfigurationBuilder() { + public static DummyConnectionConfiguration.Builder getDummyConfigurationBuilder() { return DummyConnectionConfiguration.builder().setXmppDomain(JidTestUtil.EXAMPLE_ORG).setUsernameAndPassword("dummy", "dummypass"); } @@ -77,7 +77,7 @@ public class DummyConnection extends AbstractXMPPConnection { } } - public DummyConnection(ConnectionConfiguration configuration) { + public DummyConnection(DummyConnectionConfiguration configuration) { super(configuration); for (ConnectionCreationListener listener : XMPPConnectionRegistry.getConnectionCreationListeners()) { diff --git a/smack-core/src/test/java/org/jivesoftware/smack/StanzaCollectorTest.java b/smack-core/src/test/java/org/jivesoftware/smack/StanzaCollectorTest.java index 27821a6bf..eaeb3b34e 100644 --- a/smack-core/src/test/java/org/jivesoftware/smack/StanzaCollectorTest.java +++ b/smack-core/src/test/java/org/jivesoftware/smack/StanzaCollectorTest.java @@ -19,6 +19,8 @@ package org.jivesoftware.smack; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; +import java.util.concurrent.atomic.AtomicInteger; + import org.jivesoftware.smack.filter.StanzaFilter; import org.jivesoftware.smack.packet.Stanza; @@ -55,34 +57,42 @@ public class StanzaCollectorTest { assertEquals("14", collector.pollResult().getStanzaId()); assertNull(collector.pollResult()); - assertNull(collector.nextResult(1000)); + assertNull(collector.nextResult(10)); } /** - * Although this doesn't guarentee anything due to the nature of threading, it can potentially + * Although this doesn't guarantee anything due to the nature of threading, it can potentially * catch problems. + * + * @throws InterruptedException if interrupted. */ + @SuppressWarnings("ThreadPriorityCheck") @Test - public void verifyThreadSafety() { - int insertCount = 500; + public void verifyThreadSafety() throws InterruptedException { + final int insertCount = 500; final TestStanzaCollector collector = new TestStanzaCollector(null, new OKEverything(), insertCount); + final AtomicInteger consumer1Dequeued = new AtomicInteger(); + final AtomicInteger consumer2Dequeued = new AtomicInteger(); + final AtomicInteger consumer3Dequeued = new AtomicInteger(); + Thread consumer1 = new Thread(new Runnable() { @Override public void run() { + int dequeueCount = 0; try { while (true) { - try { - Thread.sleep(3); - } catch (InterruptedException e) { - } - @SuppressWarnings("unused") + Thread.yield(); Stanza packet = collector.nextResultBlockForever(); -// System.out.println(Thread.currentThread().getName() + " packet: " + packet); + if (packet != null) { + dequeueCount++; + } } } catch (InterruptedException e) { - throw new RuntimeException(e); + // Ignore as it is expected. + } finally { + consumer1Dequeued.set(dequeueCount); } } }); @@ -92,20 +102,20 @@ public class StanzaCollectorTest { @Override public void run() { Stanza p; - + int dequeueCount = 0; do { + Thread.yield(); try { - Thread.sleep(3); - } catch (InterruptedException e) { - } - try { - p = collector.nextResult(1); + p = collector.nextResult(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } - // System.out.println(Thread.currentThread().getName() + " packet: " + p); + if (p != null) { + dequeueCount++; + } } while (p != null); + consumer2Dequeued.set(dequeueCount); } }); consumer2.setName("consumer 2"); @@ -114,37 +124,42 @@ public class StanzaCollectorTest { @Override public void run() { Stanza p; - + int dequeueCount = 0; do { - try { - Thread.sleep(3); - } catch (InterruptedException e) { - } + Thread.yield(); p = collector.pollResult(); - // System.out.println(Thread.currentThread().getName() + " packet: " + p); + if (p != null) { + dequeueCount++; + } } while (p != null); + consumer3Dequeued.set(dequeueCount); } }); consumer3.setName("consumer 3"); - consumer1.start(); - consumer2.start(); - consumer3.start(); - for (int i = 0; i < insertCount; i++) { collector.processStanza(new TestPacket(i)); } - try { - Thread.sleep(5000); - consumer3.join(); - consumer2.join(); - consumer1.interrupt(); - } catch (InterruptedException e) { - } + consumer1.start(); + consumer2.start(); + consumer3.start(); + + consumer3.join(); + consumer2.join(); + consumer1.interrupt(); + consumer1.join(); + // We cannot guarantee that this is going to pass due to the possible issue of timing between consumer 1 // and main, but the probability is extremely remote. assertNull(collector.pollResult()); + + int consumer1DequeuedLocal = consumer1Dequeued.get(); + int consumer2DequeuedLocal = consumer2Dequeued.get(); + int consumer3DequeuedLocal = consumer3Dequeued.get(); + final int totalDequeued = consumer1DequeuedLocal + consumer2DequeuedLocal + consumer3DequeuedLocal; + assertEquals("Inserted " + insertCount + " but only " + totalDequeued + " c1: " + consumer1DequeuedLocal + " c2: " + consumer2DequeuedLocal + " c3: " + + consumer3DequeuedLocal, insertCount, totalDequeued); } static class OKEverything implements StanzaFilter { 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 new file mode 100644 index 000000000..9ad9a600f --- /dev/null +++ b/smack-core/src/test/java/org/jivesoftware/smack/compress/packet/FailureTest.java @@ -0,0 +1,58 @@ +/** + * + * Copyright 2018 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.compress.packet; + +import static org.custommonkey.xmlunit.XMLAssert.assertXMLEqual; + +import java.io.IOException; + +import org.jivesoftware.smack.packet.StanzaError; +import org.jivesoftware.smack.packet.StanzaError.Condition; + +import org.junit.Test; +import org.xml.sax.SAXException; + +public class FailureTest { + + @Test + public void simpleFailureTest() throws SAXException, IOException { + Failure failure = new Failure(Failure.CompressFailureError.processing_failed); + CharSequence xml = failure.toXML(null); + + final String expectedXml = ""; + + assertXMLEqual(expectedXml, xml.toString()); + } + + @Test + public void withStanzaErrrorFailureTest() throws SAXException, IOException { + StanzaError stanzaError = StanzaError.getBuilder() + .setCondition(Condition.bad_request) + .build(); + Failure failure = new Failure(Failure.CompressFailureError.setup_failed, stanzaError); + CharSequence xml = failure.toXML(null); + + final String expectedXml = "" + + "" + + "" + + "" + + "" + + ""; + + assertXMLEqual(expectedXml, xml.toString()); + } +} diff --git a/smack-core/src/test/java/org/jivesoftware/smack/compress/provider/FailureProviderTest.java b/smack-core/src/test/java/org/jivesoftware/smack/compress/provider/FailureProviderTest.java new file mode 100644 index 000000000..6e2c3a781 --- /dev/null +++ b/smack-core/src/test/java/org/jivesoftware/smack/compress/provider/FailureProviderTest.java @@ -0,0 +1,56 @@ +/** + * + * Copyright 2018 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.compress.provider; + +import static org.junit.Assert.assertEquals; + +import org.jivesoftware.smack.compress.packet.Failure; +import org.jivesoftware.smack.packet.StanzaError; +import org.jivesoftware.smack.packet.StanzaError.Condition; +import org.jivesoftware.smack.util.PacketParserUtils; + +import org.junit.Test; +import org.xmlpull.v1.XmlPullParser; + +public class FailureProviderTest { + + @Test + public void simpleFailureTest() throws Exception { + final String xml = ""; + final XmlPullParser parser = PacketParserUtils.getParserFor(xml); + final Failure failure = FailureProvider.INSTANCE.parse(parser); + + assertEquals(Failure.CompressFailureError.processing_failed, failure.getCompressFailureError()); + } + + @Test + public void withStanzaErrrorFailureTest() throws Exception { + final String xml = "" + + "" + + "" + + "" + + "" + + ""; + final XmlPullParser parser = PacketParserUtils.getParserFor(xml); + final Failure failure = FailureProvider.INSTANCE.parse(parser); + + assertEquals(Failure.CompressFailureError.setup_failed, failure.getCompressFailureError()); + + final StanzaError error = failure.getStanzaError(); + assertEquals(Condition.bad_request, error.getCondition()); + } +} diff --git a/smack-debug-slf4j/src/main/java/org/jivesoftware/smackx/debugger/slf4j/SLF4JSmackDebugger.java b/smack-debug-slf4j/src/main/java/org/jivesoftware/smackx/debugger/slf4j/SLF4JSmackDebugger.java index 78dddef3e..1aa66efee 100644 --- a/smack-debug-slf4j/src/main/java/org/jivesoftware/smackx/debugger/slf4j/SLF4JSmackDebugger.java +++ b/smack-debug-slf4j/src/main/java/org/jivesoftware/smackx/debugger/slf4j/SLF4JSmackDebugger.java @@ -17,8 +17,6 @@ package org.jivesoftware.smackx.debugger.slf4j; -import java.io.Reader; -import java.io.Writer; import java.util.concurrent.atomic.AtomicBoolean; import org.jivesoftware.smack.AbstractXMPPConnection; @@ -88,19 +86,13 @@ public class SLF4JSmackDebugger extends SmackDebugger { } @Override - public Reader newConnectionReader(Reader newReader) { - reader.removeReaderListener(slf4JRawXmlListener); - reader = new ObservableReader(newReader); - reader.addReaderListener(slf4JRawXmlListener); - return reader; + public void outgoingStreamSink(CharSequence outgoingCharSequence) { + slf4JRawXmlListener.write(outgoingCharSequence.toString()); } @Override - public Writer newConnectionWriter(Writer newWriter) { - writer.removeWriterListener(slf4JRawXmlListener); - writer = new ObservableWriter(newWriter); - writer.addWriterListener(slf4JRawXmlListener); - return writer; + public void incomingStreamSink(CharSequence incomingCharSequence) { + slf4JRawXmlListener.read(incomingCharSequence.toString()); } @Override diff --git a/smack-debug/src/main/java/org/jivesoftware/smackx/debugger/EnhancedDebugger.java b/smack-debug/src/main/java/org/jivesoftware/smackx/debugger/EnhancedDebugger.java index b01b0d634..06bfc4318 100644 --- a/smack-debug/src/main/java/org/jivesoftware/smackx/debugger/EnhancedDebugger.java +++ b/smack-debug/src/main/java/org/jivesoftware/smackx/debugger/EnhancedDebugger.java @@ -709,21 +709,13 @@ public class EnhancedDebugger extends SmackDebugger { } @Override - public Reader newConnectionReader(Reader newReader) { - ((ObservableReader) reader).removeReaderListener(readerListener); - ObservableReader debugReader = new ObservableReader(newReader); - debugReader.addReaderListener(readerListener); - reader = debugReader; - return reader; + public final void outgoingStreamSink(CharSequence outgoingCharSequence) { + writerListener.write(outgoingCharSequence.toString()); } @Override - public Writer newConnectionWriter(Writer newWriter) { - ((ObservableWriter) writer).removeWriterListener(writerListener); - ObservableWriter debugWriter = new ObservableWriter(newWriter); - debugWriter.addWriterListener(writerListener); - writer = debugWriter; - return writer; + public final void incomingStreamSink(CharSequence incomingCharSequence) { + readerListener.read(incomingCharSequence.toString()); } @Override diff --git a/smack-debug/src/main/java/org/jivesoftware/smackx/debugger/LiteDebugger.java b/smack-debug/src/main/java/org/jivesoftware/smackx/debugger/LiteDebugger.java index 1ca75279d..a2a7c9813 100644 --- a/smack-debug/src/main/java/org/jivesoftware/smackx/debugger/LiteDebugger.java +++ b/smack-debug/src/main/java/org/jivesoftware/smackx/debugger/LiteDebugger.java @@ -304,21 +304,13 @@ public class LiteDebugger extends SmackDebugger { } @Override - public Reader newConnectionReader(Reader newReader) { - ((ObservableReader) reader).removeReaderListener(readerListener); - ObservableReader debugReader = new ObservableReader(newReader); - debugReader.addReaderListener(readerListener); - reader = debugReader; - return reader; + public void outgoingStreamSink(CharSequence outgoingCharSequence) { + writerListener.write(outgoingCharSequence.toString()); } @Override - public Writer newConnectionWriter(Writer newWriter) { - ((ObservableWriter) writer).removeWriterListener(writerListener); - ObservableWriter debugWriter = new ObservableWriter(newWriter); - debugWriter.addWriterListener(writerListener); - writer = debugWriter; - return writer; + public void incomingStreamSink(CharSequence incomingCharSequence) { + readerListener.read(incomingCharSequence.toString()); } @Override diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/gcm/provider/GcmExtensionProvider.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/gcm/provider/GcmExtensionProvider.java index 2367525bd..32a9de7f1 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/gcm/provider/GcmExtensionProvider.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/gcm/provider/GcmExtensionProvider.java @@ -1,6 +1,6 @@ /** * - * Copyright ยฉ 2014 Florian Schmaus + * Copyright 2014-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. @@ -17,13 +17,12 @@ package org.jivesoftware.smackx.gcm.provider; import org.jivesoftware.smackx.gcm.packet.GcmPacketExtension; -import org.jivesoftware.smackx.json.packet.AbstractJsonPacketExtension; import org.jivesoftware.smackx.json.provider.AbstractJsonExtensionProvider; -public class GcmExtensionProvider extends AbstractJsonExtensionProvider { +public class GcmExtensionProvider extends AbstractJsonExtensionProvider { @Override - public AbstractJsonPacketExtension from(String json) { + public GcmPacketExtension from(String json) { return new GcmPacketExtension(json); } } diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/json/provider/AbstractJsonExtensionProvider.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/json/provider/AbstractJsonExtensionProvider.java index f6e393a87..3d00ac01b 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/json/provider/AbstractJsonExtensionProvider.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/json/provider/AbstractJsonExtensionProvider.java @@ -1,6 +1,6 @@ /** * - * Copyright ยฉ 2014-2015 Florian Schmaus + * Copyright 2014-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. @@ -27,14 +27,14 @@ import org.jivesoftware.smackx.json.packet.AbstractJsonPacketExtension; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; -public abstract class AbstractJsonExtensionProvider extends ExtensionElementProvider { +public abstract class AbstractJsonExtensionProvider extends ExtensionElementProvider { @Override - public AbstractJsonPacketExtension parse(XmlPullParser parser, int initialDepth) throws XmlPullParserException, + public J parse(XmlPullParser parser, int initialDepth) throws XmlPullParserException, IOException, SmackException { String json = PacketParserUtils.parseElementText(parser); return from(json); } - public abstract AbstractJsonPacketExtension from(String json); + public abstract J from(String json); } diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/json/provider/JsonExtensionProvider.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/json/provider/JsonExtensionProvider.java index aced5235e..5e72eeae5 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/json/provider/JsonExtensionProvider.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/json/provider/JsonExtensionProvider.java @@ -1,6 +1,6 @@ /** * - * Copyright ยฉ 2014 Florian Schmaus + * Copyright 2014-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,13 +16,12 @@ */ package org.jivesoftware.smackx.json.provider; -import org.jivesoftware.smackx.json.packet.AbstractJsonPacketExtension; import org.jivesoftware.smackx.json.packet.JsonPacketExtension; -public class JsonExtensionProvider extends AbstractJsonExtensionProvider { +public class JsonExtensionProvider extends AbstractJsonExtensionProvider { @Override - public AbstractJsonPacketExtension from(String json) { + public JsonPacketExtension from(String json) { return new JsonPacketExtension(json); } } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/ping/PingManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/ping/PingManager.java index e5da32aeb..76e89369a 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/ping/PingManager.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/ping/PingManager.java @@ -20,15 +20,13 @@ import java.util.Map; import java.util.Set; import java.util.WeakHashMap; import java.util.concurrent.CopyOnWriteArraySet; -import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -import java.util.logging.Level; import java.util.logging.Logger; import org.jivesoftware.smack.AbstractConnectionClosedListener; import org.jivesoftware.smack.ConnectionCreationListener; import org.jivesoftware.smack.Manager; -import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.ScheduledAction; import org.jivesoftware.smack.SmackException.NoResponseException; import org.jivesoftware.smack.SmackException.NotConnectedException; import org.jivesoftware.smack.SmackFuture; @@ -115,7 +113,7 @@ public final class PingManager extends Manager { */ private int pingInterval = defaultPingInterval; - private ScheduledFuture nextAutomaticPing; + private ScheduledAction nextAutomaticPing; private PingManager(XMPPConnection connection) { super(connection); @@ -396,9 +394,10 @@ public final class PingManager extends Manager { } private void maybeStopPingServerTask() { + final ScheduledAction nextAutomaticPing = this.nextAutomaticPing; if (nextAutomaticPing != null) { - nextAutomaticPing.cancel(true); - nextAutomaticPing = null; + nextAutomaticPing.cancel(); + this.nextAutomaticPing = null; } } @@ -406,9 +405,7 @@ public final class PingManager extends Manager { * Ping the server if deemed necessary because automatic server pings are * enabled ({@link #setPingInterval(int)}) and the ping interval has expired. */ - public synchronized void pingServerIfNecessary() { - final int DELTA = 1000; // 1 seconds - final int TRIES = 3; // 3 tries + public void pingServerIfNecessary() { final XMPPConnection connection = connection(); if (connection == null) { // connection has been collected by GC @@ -430,45 +427,31 @@ public final class PingManager extends Manager { return; } } - if (connection.isAuthenticated()) { - boolean res = false; + if (!connection.isAuthenticated()) { + LOGGER.warning(connection + " was not authenticated"); + return; + } - for (int i = 0; i < TRIES; i++) { - if (i != 0) { - try { - Thread.sleep(DELTA); - } catch (InterruptedException e) { - // We received an interrupt - // This only happens if we should stop pinging - return; - } - } - try { - res = pingMyServer(false); - } - catch (InterruptedException | SmackException e) { - // Note that we log the connection here, so that it is not GC'ed between the call to isAuthenticated - // a few lines above and the usage of the connection within pingMyServer(). In order to prevent: - // https://community.igniterealtime.org/thread/59369 - LOGGER.log(Level.WARNING, "Exception while pinging server of " + connection, e); - res = false; - } - // stop when we receive a pong back - if (res) { - break; - } - } - if (!res) { - for (PingFailedListener l : pingFailedListeners) { - l.pingFailed(); - } - } else { + final long minimumTimeout = TimeUnit.MINUTES.toMillis(2); + final long connectionReplyTimeout = connection.getReplyTimeout(); + final long timeout = connectionReplyTimeout > minimumTimeout ? connectionReplyTimeout : minimumTimeout; + + SmackFuture pingFuture = pingAsync(connection.getXMPPServiceDomain(), timeout); + pingFuture.onSuccess(new SuccessCallback() { + @Override + public void onSuccess(Boolean result) { // Ping was successful, wind-up the periodic task again maybeSchedulePingServerTask(); } - } else { - LOGGER.warning("XMPPConnection was not authenticated"); - } + }); + pingFuture.onError(new ExceptionCallback() { + @Override + public void processException(Exception exception) { + for (PingFailedListener l : pingFailedListeners) { + l.pingFailed(); + } + } + }); } private final Runnable pingServerRunnable = new Runnable() { diff --git a/smack-im/src/test/java/org/jivesoftware/smack/roster/RosterVersioningTest.java b/smack-im/src/test/java/org/jivesoftware/smack/roster/RosterVersioningTest.java index 5b086f33a..f3eeb3b23 100644 --- a/smack-im/src/test/java/org/jivesoftware/smack/roster/RosterVersioningTest.java +++ b/smack-im/src/test/java/org/jivesoftware/smack/roster/RosterVersioningTest.java @@ -26,7 +26,6 @@ import java.io.IOException; import java.util.Collection; import java.util.HashSet; -import org.jivesoftware.smack.ConnectionConfiguration; import org.jivesoftware.smack.DummyConnection; import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.XMPPException; @@ -71,8 +70,7 @@ public class RosterVersioningTest { DirectoryRosterStore store = DirectoryRosterStore.init(tmpFolder.newFolder("store")); populateStore(store); - ConnectionConfiguration.Builder builder = DummyConnection.getDummyConfigurationBuilder(); - connection = new DummyConnection(builder.build()); + connection = new DummyConnection(); connection.connect(); connection.login(); rosterListener = new TestRosterListener(); diff --git a/smack-integration-test/build.gradle b/smack-integration-test/build.gradle index 23c50bf7d..ab813a5d4 100644 --- a/smack-integration-test/build.gradle +++ b/smack-integration-test/build.gradle @@ -15,7 +15,7 @@ dependencies { compile project(':smack-openpgp') compile project(':smack-debug') compile project(path: ":smack-omemo", configuration: "testRuntime") - compile 'org.reflections:reflections:0.9.9-RC1' + compile 'org.reflections:reflections:0.9.11' compile 'eu.geekplace.javapinning:java-pinning-java7:1.1.0-alpha1' // Note that the junit-vintage-engine runtime dependency is not // directly required, but it declares a dependency to diff --git a/smack-integration-test/src/main/java/org/igniterealtime/smack/XmppConnectionStressTest.java b/smack-integration-test/src/main/java/org/igniterealtime/smack/XmppConnectionStressTest.java new file mode 100644 index 000000000..2574a3970 --- /dev/null +++ b/smack-integration-test/src/main/java/org/igniterealtime/smack/XmppConnectionStressTest.java @@ -0,0 +1,266 @@ +/** + * + * Copyright 2018-2019 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.igniterealtime.smack; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; + +import org.jivesoftware.smack.SmackException.NotConnectedException; +import org.jivesoftware.smack.StanzaListener; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.filter.AndFilter; +import org.jivesoftware.smack.filter.MessageTypeFilter; +import org.jivesoftware.smack.filter.StanzaExtensionFilter; +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.packet.Stanza; +import org.jivesoftware.smack.util.Async; +import org.jivesoftware.smack.util.MultiMap; +import org.jivesoftware.smack.util.StringUtils; + +import org.jivesoftware.smackx.jiveproperties.JivePropertiesManager; +import org.jivesoftware.smackx.jiveproperties.packet.JivePropertiesExtension; + +import org.igniterealtime.smack.XmppConnectionStressTest.StressTestFailedException.ErrorsWhileSendingOrReceivingException; +import org.igniterealtime.smack.XmppConnectionStressTest.StressTestFailedException.NotAllMessagesReceivedException; +import org.jxmpp.jid.EntityFullJid; + +public class XmppConnectionStressTest { + + private static final String MESSAGE_NUMBER_PROPERTY = "message-number"; + + public static class Configuration { + public final long seed; + public final int messagesPerConnection; + public final int maxPayloadChunkSize; + public final int maxPayloadChunks; + public final boolean intermixMessages; + + public Configuration(long seed, int messagesPerConnection, int maxPayloadChunkSize, int maxPayloadChunks, + boolean intermixMessages) { + this.seed = seed; + this.messagesPerConnection = messagesPerConnection; + this.maxPayloadChunkSize = maxPayloadChunkSize; + this.maxPayloadChunks = maxPayloadChunks; + this.intermixMessages = intermixMessages; + } + } + + private final Configuration configuration; + + public XmppConnectionStressTest(Configuration configuration) { + this.configuration = configuration; + } + + private volatile long waitStart; + + public void run(List connections, final long replyTimeoutMillis) + throws InterruptedException, NotAllMessagesReceivedException, ErrorsWhileSendingOrReceivingException { + final MultiMap messages = new MultiMap<>(); + final Random random = new Random(configuration.seed); + final Map sendExceptions = new ConcurrentHashMap<>(); + final Map receiveExceptions = new ConcurrentHashMap<>(); + + waitStart = -1; + + for (XMPPConnection fromConnection : connections) { + MultiMap toConnectionMessages = new MultiMap<>(); + for (XMPPConnection toConnection : connections) { + for (int i = 0; i < configuration.messagesPerConnection; i++) { + Message message = new Message(); + message.setTo(toConnection.getUser()); + + int payloadChunkCount = random.nextInt(configuration.maxPayloadChunks) + 1; + for (int c = 0; c < payloadChunkCount; c++) { + int payloadChunkSize = random.nextInt(configuration.maxPayloadChunkSize) + 1; + String payloadCunk = StringUtils.randomString(payloadChunkSize, random); + JivePropertiesManager.addProperty(message, "payload-chunk-" + c, payloadCunk); + } + + JivePropertiesManager.addProperty(message, MESSAGE_NUMBER_PROPERTY, i); + + toConnectionMessages.put(toConnection, message); + } + } + + if (configuration.intermixMessages) { + while (!toConnectionMessages.isEmpty()) { + int next = random.nextInt(connections.size()); + Message message = null; + while (message == null) { + XMPPConnection toConnection = connections.get(next); + message = toConnectionMessages.getFirst(toConnection); + next = (next + 1) % connections.size(); + } + messages.put(fromConnection, message); + } + } else { + for (XMPPConnection toConnection : connections) { + for (Message message : toConnectionMessages.getAll(toConnection)) { + messages.put(fromConnection, message); + } + } + } + } + + Semaphore receivedSemaphore = new Semaphore(-connections.size() + 1); + Map> receiveMarkers = new ConcurrentHashMap<>(connections.size()); + + for (XMPPConnection connection : connections) { + connection.addSyncStanzaListener(new StanzaListener() { + @Override + public void processStanza(Stanza stanza) { + waitStart = System.currentTimeMillis(); + + EntityFullJid from = stanza.getFrom().asEntityFullJidOrThrow(); + Message message = (Message) stanza; + JivePropertiesExtension extension = JivePropertiesExtension.from(message); + + Integer messageNumber = (Integer) extension.getProperty(MESSAGE_NUMBER_PROPERTY); + + Map myReceiveMarkers = receiveMarkers.get(connection); + if (myReceiveMarkers == null) { + myReceiveMarkers = new HashMap<>(connections.size()); + receiveMarkers.put(connection, myReceiveMarkers); + } + + boolean[] fromMarkers = myReceiveMarkers.get(from); + if (fromMarkers == null) { + fromMarkers = new boolean[configuration.messagesPerConnection]; + myReceiveMarkers.put(from, fromMarkers); + } + + // Sanity check: All markers before must be true, all markers including the messageNumber marker must be false. + for (int i = 0; i < fromMarkers.length; i++) { + if ((i < messageNumber && !fromMarkers[i]) + || (i >= messageNumber && fromMarkers[i])) { + // TODO: Better exception. + Exception exception = new Exception("out of order"); + receiveExceptions.put(connection, exception); + // TODO: Current Smack design does not guarantee that the listener won't be invoked again. + // This is because the decission to invoke a sync listeners is done at a different place + // then invoking the listener. + connection.removeSyncStanzaListener(this); + receivedSemaphore.release(); + return; + } + } + + fromMarkers[messageNumber] = true; + + if (myReceiveMarkers.size() != connections.size()) { + return; + } + + for (boolean[] markers : myReceiveMarkers.values()) { + for (boolean b : markers) { + if (!b) { + return; + } + } + } + // All markers set to true, this means we received all messages. + receivedSemaphore.release(); + } + }, new AndFilter(MessageTypeFilter.NORMAL, + new StanzaExtensionFilter(JivePropertiesExtension.ELEMENT, JivePropertiesExtension.NAMESPACE))); + } + + Semaphore sendSemaphore = new Semaphore(-connections.size() + 1); + + for (XMPPConnection connection : connections) { + Async.go(() -> { + List messagesToSend; + synchronized (messages) { + messagesToSend = messages.getAll(connection); + } + try { + for (Message messageToSend : messagesToSend) { + connection.sendStanza(messageToSend); + } + } catch (NotConnectedException | InterruptedException e) { + sendExceptions.put(connection, e); + } finally { + sendSemaphore.release(); + } + }); + } + + sendSemaphore.acquire(); + + if (waitStart < 0) { + waitStart = System.currentTimeMillis(); + } + + boolean acquired; + do { + long acquireWait = waitStart + replyTimeoutMillis - System.currentTimeMillis(); + acquired = receivedSemaphore.tryAcquire(acquireWait, TimeUnit.MILLISECONDS); + } while (!acquired && System.currentTimeMillis() < waitStart + replyTimeoutMillis); + + if (!acquired && receiveExceptions.isEmpty() && sendExceptions.isEmpty()) { + throw new StressTestFailedException.NotAllMessagesReceivedException(receiveMarkers); + } + + if (!receiveExceptions.isEmpty() || !sendExceptions.isEmpty()) { + throw new StressTestFailedException.ErrorsWhileSendingOrReceivingException(sendExceptions, + receiveExceptions); + } + + // Test successful. + } + + public abstract static class StressTestFailedException extends Exception { + + private static final long serialVersionUID = 1L; + + protected StressTestFailedException(String message) { + super(message); + } + + public static final class NotAllMessagesReceivedException extends StressTestFailedException { + + private static final long serialVersionUID = 1L; + + public final Map> receiveMarkers; + + private NotAllMessagesReceivedException(Map> receiveMarkers) { + super("Did not receive all messages"); + this.receiveMarkers = receiveMarkers; + } + } + + public static final class ErrorsWhileSendingOrReceivingException extends StressTestFailedException { + + private static final long serialVersionUID = 1L; + + public final Map sendExceptions; + public final Map receiveExceptions; + + private ErrorsWhileSendingOrReceivingException(Map sendExceptions, + Map receiveExceptions) { + super("Exceptions while sending and/or receiving"); + this.sendExceptions = sendExceptions; + this.receiveExceptions = receiveExceptions; + } + } + } +} diff --git a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/AbstractSmackIntTest.java b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/AbstractSmackIntTest.java index 3e39ad2ce..c96f090a8 100644 --- a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/AbstractSmackIntTest.java +++ b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/AbstractSmackIntTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015-2016 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. @@ -45,10 +45,10 @@ public abstract class AbstractSmackIntTest { protected final Configuration sinttestConfiguration; - protected AbstractSmackIntTest(String testRunId, Configuration configuration) { - this.testRunId = testRunId; - this.sinttestConfiguration = configuration; - this.timeout = configuration.replyTimeout; + protected AbstractSmackIntTest(SmackIntegrationTestEnvironment environment) { + this.testRunId = environment.testRunId; + this.sinttestConfiguration = environment.configuration; + this.timeout = environment.configuration.replyTimeout; } protected void performActionAndWaitUntilStanzaReceived(Runnable action, XMPPConnection connection, StanzaFilter filter) diff --git a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/AbstractSmackIntegrationTest.java b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/AbstractSmackIntegrationTest.java index 91ccba353..5f32f9268 100644 --- a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/AbstractSmackIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/AbstractSmackIntegrationTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015 Florian Schmaus + * Copyright 2015-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,6 +16,10 @@ */ package org.igniterealtime.smack.inttest; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + import org.jivesoftware.smack.XMPPConnection; public abstract class AbstractSmackIntegrationTest extends AbstractSmackIntTest { @@ -40,10 +44,18 @@ public abstract class AbstractSmackIntegrationTest extends AbstractSmackIntTest */ protected final XMPPConnection connection; - public AbstractSmackIntegrationTest(SmackIntegrationTestEnvironment environment) { - super(environment.testRunId, environment.configuration); + protected final List connections; + + public AbstractSmackIntegrationTest(SmackIntegrationTestEnvironment environment) { + super(environment); this.connection = this.conOne = environment.conOne; this.conTwo = environment.conTwo; this.conThree = environment.conThree; + + final List connectionsLocal = new ArrayList<>(3); + connectionsLocal.add(conOne); + connectionsLocal.add(conTwo); + connectionsLocal.add(conThree); + this.connections = Collections.unmodifiableList(connectionsLocal); } } diff --git a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/AbstractSmackLowLevelIntegrationTest.java b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/AbstractSmackLowLevelIntegrationTest.java index 58fc992ea..cd0b89b48 100644 --- a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/AbstractSmackLowLevelIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/AbstractSmackLowLevelIntegrationTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015-2017 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. @@ -16,17 +16,22 @@ */ package org.igniterealtime.smack.inttest; -import java.security.KeyManagementException; -import java.security.NoSuchAlgorithmException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; -import org.jivesoftware.smack.tcp.XMPPTCPConnection; -import org.jivesoftware.smack.tcp.XMPPTCPConnectionConfiguration; +import org.jivesoftware.smack.AbstractXMPPConnection; +import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.SmackException.NoResponseException; +import org.jivesoftware.smack.SmackException.NotConnectedException; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.XMPPException.XMPPErrorException; import org.jxmpp.jid.DomainBareJid; public abstract class AbstractSmackLowLevelIntegrationTest extends AbstractSmackIntTest { - private final SmackIntegrationTestEnvironment environment; + private final SmackIntegrationTestEnvironment environment; /** * The configuration @@ -35,33 +40,36 @@ public abstract class AbstractSmackLowLevelIntegrationTest extends AbstractSmack protected final DomainBareJid service; - public AbstractSmackLowLevelIntegrationTest(SmackIntegrationTestEnvironment environment) { - super(environment.testRunId, environment.configuration); + protected AbstractSmackLowLevelIntegrationTest(SmackIntegrationTestEnvironment environment) { + super(environment); this.environment = environment; this.configuration = environment.configuration; this.service = configuration.service; } - public final XMPPTCPConnectionConfiguration.Builder getConnectionConfiguration() throws KeyManagementException, NoSuchAlgorithmException { - XMPPTCPConnectionConfiguration.Builder builder = XMPPTCPConnectionConfiguration.builder(); - if (configuration.tlsContext != null) { - builder.setCustomSSLContext(configuration.tlsContext); - } - builder.setSecurityMode(configuration.securityMode); - builder.setXmppDomain(service); - return builder; + protected AbstractXMPPConnection getConnectedConnection() throws InterruptedException, XMPPException, SmackException, IOException { + AbstractXMPPConnection connection = getUnconnectedConnection(); + connection.connect().login(); + return connection; } - protected void performCheck(ConnectionCallback callback) throws Exception { - XMPPTCPConnection connection = SmackIntegrationTestFramework.getConnectedConnection(environment, -1); - try { - callback.connectionCallback(connection); - } finally { - IntTestUtil.disconnectAndMaybeDelete(connection, configuration); - } + protected AbstractXMPPConnection getUnconnectedConnection() + throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { + return environment.connectionManager.constructConnection(); } - public interface ConnectionCallback { - void connectionCallback(XMPPTCPConnection connection) throws Exception; + protected List getUnconnectedConnections(int count) + throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { + List connections = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + AbstractXMPPConnection connection = getUnconnectedConnection(); + connections.add(connection); + } + return connections; } + + protected void recycle(AbstractXMPPConnection connection) { + environment.connectionManager.recycle(connection); + } + } diff --git a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/AbstractSmackSpecificLowLevelIntegrationTest.java b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/AbstractSmackSpecificLowLevelIntegrationTest.java new file mode 100644 index 000000000..6c9bbe219 --- /dev/null +++ b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/AbstractSmackSpecificLowLevelIntegrationTest.java @@ -0,0 +1,63 @@ +/** + * + * Copyright 2018-2019 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.igniterealtime.smack.inttest; + +import java.util.ArrayList; +import java.util.List; + +import org.jivesoftware.smack.AbstractXMPPConnection; +import org.jivesoftware.smack.ConnectionConfiguration; +import org.jivesoftware.smack.SmackException.NoResponseException; +import org.jivesoftware.smack.SmackException.NotConnectedException; +import org.jivesoftware.smack.XMPPException.XMPPErrorException; + +public abstract class AbstractSmackSpecificLowLevelIntegrationTest + extends AbstractSmackLowLevelIntegrationTest { + + private final SmackIntegrationTestEnvironment environment; + + protected final Class connectionClass; + + private final XmppConnectionDescriptor> connectionDescriptor; + + public AbstractSmackSpecificLowLevelIntegrationTest(SmackIntegrationTestEnvironment environment, + Class connectionClass) { + super(environment); + this.environment = environment; + this.connectionClass = connectionClass; + + connectionDescriptor = environment.connectionManager.getConnectionDescriptorFor(connectionClass); + } + + public Class getConnectionClass() { + return connectionClass; + } + + protected C getSpecificUnconnectedConnection() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { + return environment.connectionManager.constructConnection(connectionDescriptor); + } + + protected List getSpecificUnconnectedConnections(int count) + throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { + List connections = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + C connection = getSpecificUnconnectedConnection(); + connections.add(connection); + } + return connections; + } +} diff --git a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/Configuration.java b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/Configuration.java index 560deb6c5..d18cfe487 100644 --- a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/Configuration.java +++ b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/Configuration.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015-2017 Florian Schmaus + * Copyright 2015-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. @@ -32,14 +32,18 @@ import java.util.logging.Logger; import javax.net.ssl.SSLContext; import org.jivesoftware.smack.ConnectionConfiguration.SecurityMode; +import org.jivesoftware.smack.debugger.ConsoleDebugger; import org.jivesoftware.smack.util.Objects; import org.jivesoftware.smack.util.StringUtils; +import org.jivesoftware.smackx.debugger.EnhancedDebugger; + import eu.geekplace.javapinning.java7.Java7Pinning; import org.jxmpp.jid.DomainBareJid; import org.jxmpp.jid.impl.JidCreate; import org.jxmpp.stringprep.XmppStringprepException; +// TODO: Rename to SinttestConfiguration. public final class Configuration { private static final Logger LOGGER = Logger.getLogger(Configuration.class.getName()); @@ -92,6 +96,8 @@ public final class Configuration { public final Set testPackages; + public final ConnectionConfigurationBuilderApplier configurationApplier; + private Configuration(DomainBareJid service, String serviceTlsPin, SecurityMode securityMode, int replyTimeout, Debugger debugger, String accountOneUsername, String accountOnePassword, String accountTwoUsername, String accountTwoPassword, String accountThreeUsername, String accountThreePassword, Set enabledTests, Set disabledTests, @@ -126,6 +132,13 @@ public final class Configuration { this.adminAccountUsername = adminAccountUsername; this.adminAccountPassword = adminAccountPassword; + boolean accountOnePasswordSet = StringUtils.isNotEmpty(accountOnePassword); + if (accountOnePasswordSet != StringUtils.isNotEmpty(accountTwoPassword) || + accountOnePasswordSet != StringUtils.isNotEmpty(accountThreePassword)) { + // Ensure the invariant that either all main accounts have a password set, or none. + throw new IllegalArgumentException(); + } + this.accountOneUsername = accountOneUsername; this.accountOnePassword = accountOnePassword; this.accountTwoUsername = accountTwoUsername; @@ -135,6 +148,26 @@ public final class Configuration { this.enabledTests = enabledTests; this.disabledTests = disabledTests; this.testPackages = testPackages; + + this.configurationApplier = (builder) -> { + if (tlsContext != null) { + builder.setCustomSSLContext(tlsContext); + } + builder.setSecurityMode(securityMode); + builder.setXmppDomain(service); + + switch (debugger) { + case enhanced: + builder.setDebuggerFactory(EnhancedDebugger.Factory.INSTANCE); + break; + case console: + builder.setDebuggerFactory(ConsoleDebugger.Factory.INSTANCE); + break; + case none: + // Nothing to do :). + break; + } + }; } public boolean isAccountRegistrationPossible() { diff --git a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/ConnectionConfigurationBuilderApplier.java b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/ConnectionConfigurationBuilderApplier.java new file mode 100644 index 000000000..1ae512369 --- /dev/null +++ b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/ConnectionConfigurationBuilderApplier.java @@ -0,0 +1,23 @@ +/** + * + * Copyright 2018 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.igniterealtime.smack.inttest; + +import org.jivesoftware.smack.ConnectionConfiguration; + +public interface ConnectionConfigurationBuilderApplier { + void applyConfigurationTo(ConnectionConfiguration.Builder connectionConfigurationBuilder); +} diff --git a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/FailedTest.java b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/FailedTest.java index 2d5c64696..937ff3e6b 100644 --- a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/FailedTest.java +++ b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/FailedTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015 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. @@ -16,15 +16,14 @@ */ package org.igniterealtime.smack.inttest; -import java.lang.reflect.Method; import java.util.List; public class FailedTest extends TestResult { public final Throwable failureReason; - public FailedTest(Method testMethod, long startTime, long endTime, List logMessages, Throwable failureReason) { - super(testMethod, startTime, endTime, logMessages); + public FailedTest(SmackIntegrationTestFramework.ConcreteTest concreteTest, long startTime, long endTime, List logMessages, Throwable failureReason) { + super(concreteTest, startTime, endTime, logMessages); this.failureReason = failureReason; } } diff --git a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/IntTestUtil.java b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/IntTestUtil.java deleted file mode 100644 index ff91480de..000000000 --- a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/IntTestUtil.java +++ /dev/null @@ -1,241 +0,0 @@ -/** - * - * Copyright 2015-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. - * 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.igniterealtime.smack.inttest; - -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; -import java.util.logging.Level; -import java.util.logging.Logger; - -import org.jivesoftware.smack.SmackException; -import org.jivesoftware.smack.SmackException.NoResponseException; -import org.jivesoftware.smack.SmackException.NotConnectedException; -import org.jivesoftware.smack.XMPPConnection; -import org.jivesoftware.smack.XMPPException; -import org.jivesoftware.smack.XMPPException.XMPPErrorException; -import org.jivesoftware.smack.tcp.XMPPTCPConnection; -import org.jivesoftware.smack.util.StringUtils; - -import org.jivesoftware.smackx.admin.ServiceAdministrationManager; -import org.jivesoftware.smackx.iqregister.AccountManager; - -import org.igniterealtime.smack.inttest.Configuration.AccountRegistration; -import org.jxmpp.jid.EntityBareJid; -import org.jxmpp.jid.impl.JidCreate; -import org.jxmpp.jid.parts.Localpart; -import org.jxmpp.stringprep.XmppStringprepException; - -public class IntTestUtil { - - private static final Logger LOGGER = Logger.getLogger(IntTestUtil.class.getName()); - - - public static UsernameAndPassword registerAccount(XMPPTCPConnection connection, SmackIntegrationTestEnvironment environment, int connectionId) throws InterruptedException, XMPPException, SmackException, IOException { - String username = "sinttest-" + environment.testRunId + "-" + connectionId; - return registerAccount(connection, username, StringUtils.insecureRandomString(12), environment.configuration); - } - - public static UsernameAndPassword registerAccount(XMPPTCPConnection connection, String accountUsername, String accountPassword, - Configuration config) throws InterruptedException, XMPPException, SmackException, IOException { - switch (config.accountRegistration) { - case inBandRegistration: - return registerAccountViaIbr(connection, accountUsername, accountPassword); - case serviceAdministration: - return registerAccountViaAdmin(connection, accountUsername, accountPassword, config.adminAccountUsername, config.adminAccountPassword); - default: - throw new AssertionError(); - } - } - -// public static UsernameAndPassword registerAccountViaAdmin(XMPPTCPConnection connection) throws XmppStringprepException, NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { -// return registerAccountViaAdmin(connection, StringUtils.insecureRandomString(12), -// StringUtils.insecureRandomString(12)); -// } - - public static UsernameAndPassword registerAccountViaAdmin(XMPPTCPConnection connection, String username, - String password, String adminAccountUsername, String adminAccountPassword) throws InterruptedException, XMPPException, SmackException, IOException { - connection.login(adminAccountUsername, adminAccountPassword); - - ServiceAdministrationManager adminManager = ServiceAdministrationManager.getInstanceFor(connection); - - EntityBareJid userJid = JidCreate.entityBareFrom(Localpart.from(username), connection.getXMPPServiceDomain()); - adminManager.addUser(userJid, password); - - connection.disconnect(); - connection.connect(); - - return new UsernameAndPassword(username, password); - - } - - public static UsernameAndPassword registerAccountViaIbr(XMPPConnection connection) - throws NoResponseException, XMPPErrorException, NotConnectedException, - InterruptedException { - return registerAccountViaIbr(connection, StringUtils.insecureRandomString(12), - StringUtils.insecureRandomString(12)); - } - - public static UsernameAndPassword registerAccountViaIbr(XMPPConnection connection, String username, - String password) throws NoResponseException, XMPPErrorException, - NotConnectedException, InterruptedException { - AccountManager accountManager = AccountManager.getInstance(connection); - if (!accountManager.supportsAccountCreation()) { - throw new UnsupportedOperationException("Account creation/registation is not supported"); - } - Set requiredAttributes = accountManager.getAccountAttributes(); - if (requiredAttributes.size() > 4) { - throw new IllegalStateException("Unkown required attributes"); - } - Map additionalAttributes = new HashMap<>(); - additionalAttributes.put("name", "Smack Integration Test"); - additionalAttributes.put("email", "flow@igniterealtime.org"); - Localpart usernameLocalpart; - try { - usernameLocalpart = Localpart.from(username); - } - catch (XmppStringprepException e) { - throw new IllegalArgumentException("Invalid username: " + username, e); - } - accountManager.createAccount(usernameLocalpart, password, additionalAttributes); - - return new UsernameAndPassword(username, password); - } - - public static final class UsernameAndPassword { - public final String username; - public final String password; - - private UsernameAndPassword(String username, String password) { - this.username = username; - this.password = password; - } - } - - - public static void disconnectAndMaybeDelete(XMPPTCPConnection connection, Configuration config) throws InterruptedException { - try { - if (!config.isAccountRegistrationPossible()) { - return; - } - - Configuration.AccountRegistration accountDeletionMethod = config.accountRegistration; - - AccountManager accountManager = AccountManager.getInstance(connection); - try { - if (accountManager.isSupported()) { - accountDeletionMethod = AccountRegistration.inBandRegistration; - } - } - catch (NoResponseException | XMPPErrorException | NotConnectedException e) { - LOGGER.log(Level.WARNING, "Could not test if XEP-0077 account deletion is possible", e); - } - - switch (accountDeletionMethod) { - case inBandRegistration: - deleteViaIbr(connection); - break; - case serviceAdministration: - deleteViaServiceAdministration(connection, config); - break; - default: - throw new AssertionError(); - } - } - finally { - connection.disconnect(); - } - } - - public static void deleteViaServiceAdministration(XMPPTCPConnection connection, Configuration config) { - EntityBareJid accountToDelete = connection.getUser().asEntityBareJid(); - - final int maxAttempts = 3; - - int attempts; - for (attempts = 0; attempts < maxAttempts; attempts++) { - connection.disconnect(); - - try { - connection.connect().login(config.adminAccountUsername, config.adminAccountPassword); - } - catch (XMPPException | SmackException | IOException | InterruptedException e) { - LOGGER.log(Level.WARNING, "Exception deleting account for " + connection, e); - continue; - } - - ServiceAdministrationManager adminManager = ServiceAdministrationManager.getInstanceFor(connection); - try { - adminManager.deleteUser(accountToDelete); - break; - } - catch (NoResponseException | XMPPErrorException | NotConnectedException | InterruptedException e) { - LOGGER.log(Level.WARNING, "Exception deleting account for " + connection, e); - continue; - } - } - if (attempts > maxAttempts) { - LOGGER.log(Level.SEVERE, "Could not delete account for connection: " + connection); - } - } - - public static void deleteViaIbr(XMPPTCPConnection connection) - throws InterruptedException { - // If the connection is disconnected, then re-reconnect and login. This could happen when - // (low-level) integration tests disconnect the connection, e.g. to test disconnection - // mechanisms - if (!connection.isConnected()) { - try { - connection.connect().login(); - } - catch (XMPPException | SmackException | IOException e) { - LOGGER.log(Level.WARNING, "Exception reconnection account for deletion", e); - } - } - - final int maxAttempts = 3; - AccountManager am = AccountManager.getInstance(connection); - int attempts; - for (attempts = 0; attempts < maxAttempts; attempts++) { - try { - am.deleteAccount(); - } - catch (XMPPErrorException | NoResponseException e) { - LOGGER.log(Level.WARNING, "Exception deleting account for " + connection, e); - continue; - } - catch (NotConnectedException e) { - LOGGER.log(Level.WARNING, "Exception deleting account for " + connection, e); - try { - connection.connect().login(); - } - catch (XMPPException | SmackException | IOException e2) { - LOGGER.log(Level.WARNING, "Exception while trying to re-connect " + connection, e); - } - continue; - } - LOGGER.info("Successfully deleted account of " + connection); - break; - } - if (attempts > maxAttempts) { - LOGGER.log(Level.SEVERE, "Could not delete account for connection: " + connection); - } - - } - -} diff --git a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/SmackIntegrationTest.java b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/SmackIntegrationTest.java index 1f57a2f7a..bcbfb8e35 100644 --- a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/SmackIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/SmackIntegrationTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015 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. @@ -16,13 +16,19 @@ */ package org.igniterealtime.smack.inttest; +import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface SmackIntegrationTest { + boolean onlyDefaultConnectionType() default false; + + int connectionCount() default -1; + } diff --git a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/SmackIntegrationTestEnvironment.java b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/SmackIntegrationTestEnvironment.java index ccfe2f131..58dae7fb9 100644 --- a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/SmackIntegrationTestEnvironment.java +++ b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/SmackIntegrationTestEnvironment.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015 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. @@ -16,26 +16,25 @@ */ package org.igniterealtime.smack.inttest; -import org.jivesoftware.smack.tcp.XMPPTCPConnection; +import org.jivesoftware.smack.AbstractXMPPConnection; -public class SmackIntegrationTestEnvironment { +public class SmackIntegrationTestEnvironment { - public final XMPPTCPConnection conOne; - - public final XMPPTCPConnection conTwo; - - public final XMPPTCPConnection conThree; + public final C conOne, conTwo, conThree; public final String testRunId; public final Configuration configuration; - SmackIntegrationTestEnvironment(XMPPTCPConnection conOne, XMPPTCPConnection conTwo, XMPPTCPConnection conThree, String testRunId, - Configuration configuration) { + public final XmppConnectionManager connectionManager; + + SmackIntegrationTestEnvironment(C conOne, C conTwo, C conThree, String testRunId, + Configuration configuration, XmppConnectionManager connectionManager) { this.conOne = conOne; this.conTwo = conTwo; this.conThree = conThree; this.testRunId = testRunId; this.configuration = configuration; + this.connectionManager = connectionManager; } } 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 287163939..5ff2b1e82 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 @@ -1,6 +1,6 @@ /** * - * Copyright 2015-2017 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. @@ -27,8 +27,12 @@ import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -43,21 +47,20 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import java.util.logging.Logger; +import org.jivesoftware.smack.AbstractXMPPConnection; import org.jivesoftware.smack.ConnectionConfiguration.SecurityMode; import org.jivesoftware.smack.SmackConfiguration; import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.SmackException.NoResponseException; import org.jivesoftware.smack.XMPPException; -import org.jivesoftware.smack.debugger.ConsoleDebugger; import org.jivesoftware.smack.tcp.XMPPTCPConnection; import org.jivesoftware.smack.tcp.XMPPTCPConnectionConfiguration; import org.jivesoftware.smack.util.StringUtils; -import org.jivesoftware.smackx.debugger.EnhancedDebugger; import org.jivesoftware.smackx.debugger.EnhancedDebuggerWindow; import org.jivesoftware.smackx.iqregister.AccountManager; -import org.igniterealtime.smack.inttest.IntTestUtil.UsernameAndPassword; +import org.igniterealtime.smack.inttest.Configuration.AccountRegistration; import org.junit.AfterClass; import org.junit.BeforeClass; import org.reflections.Reflections; @@ -66,51 +69,60 @@ import org.reflections.scanners.MethodParameterScanner; import org.reflections.scanners.SubTypesScanner; import org.reflections.scanners.TypeAnnotationsScanner; -public class SmackIntegrationTestFramework { +public class SmackIntegrationTestFramework { private static final Logger LOGGER = Logger.getLogger(SmackIntegrationTestFramework.class.getName()); - private static final char CLASS_METHOD_SEP = '#'; + + public static boolean SINTTEST_UNIT_TEST = false; + + private final Class defaultConnectionClass; protected final Configuration config; + protected TestRunResult testRunResult; - private SmackIntegrationTestEnvironment environment; + + private SmackIntegrationTestEnvironment environment; + protected XmppConnectionManager connectionManager; public enum TestType { Normal, LowLevel, + SpecificLowLevel, } public static void main(String[] args) throws IOException, KeyManagementException, - NoSuchAlgorithmException, SmackException, XMPPException, InterruptedException { + NoSuchAlgorithmException, SmackException, XMPPException, InterruptedException, InstantiationException, + IllegalAccessException, IllegalArgumentException, InvocationTargetException { Configuration config = Configuration.newConfiguration(args); - SmackIntegrationTestFramework sinttest = new SmackIntegrationTestFramework(config); + SmackIntegrationTestFramework sinttest = new SmackIntegrationTestFramework<>(config, XMPPTCPConnection.class); TestRunResult testRunResult = sinttest.run(); - for (Entry, String> entry : testRunResult.impossibleTestClasses.entrySet()) { + for (Entry, Throwable> entry : testRunResult.impossibleTestClasses.entrySet()) { LOGGER.info("Could not run " + entry.getKey().getName() + " because: " - + entry.getValue()); + + entry.getValue().getLocalizedMessage()); } - for (TestNotPossible testNotPossible : testRunResult.impossibleTestMethods) { - LOGGER.info("Could not run " + testNotPossible.testMethod.getName() + " because: " + for (TestNotPossible testNotPossible : testRunResult.impossibleIntegrationTests) { + LOGGER.info("Could not run " + testNotPossible.concreteTest + " because: " + testNotPossible.testNotPossibleException.getMessage()); } - final int successfulTests = testRunResult.successfulTests.size(); + for (SuccessfulTest successfulTest : testRunResult.successfulIntegrationTests) { + LOGGER.info(successfulTest.concreteTest + " โœ”"); + } + final int successfulTests = testRunResult.successfulIntegrationTests.size(); + final int failedTests = testRunResult.failedIntegrationTests.size(); + final int totalIntegrationTests = successfulTests + failedTests; final int availableTests = testRunResult.getNumberOfAvailableTests(); final int possibleTests = testRunResult.getNumberOfPossibleTests(); LOGGER.info("SmackIntegrationTestFramework[" + testRunResult.testRunId + ']' + ": Finished [" - + successfulTests + '/' + possibleTests + "] (of " + availableTests + " available tests)"); + + successfulTests + '/' + totalIntegrationTests + "] (" + possibleTests + " test methods of " + availableTests + " where possible)"); - int exitStatus; - if (!testRunResult.failedIntegrationTests.isEmpty()) { - final int failedTests = testRunResult.failedIntegrationTests.size(); - LOGGER.warning("The following " + failedTests + " tests failed!"); + final int exitStatus; + if (failedTests > 0) { + LOGGER.warning("๐Ÿ’€ The following " + failedTests + " tests failed! ๐Ÿ’€"); for (FailedTest failedTest : testRunResult.failedIntegrationTests) { - final Method method = failedTest.testMethod; - final String className = method.getDeclaringClass().getName(); - final String methodName = method.getName(); final Throwable cause = failedTest.failureReason; - LOGGER.severe(className + CLASS_METHOD_SEP + methodName + " failed: " + cause); + LOGGER.log(Level.SEVERE, failedTest.concreteTest + " failed: " + cause, cause); } exitStatus = 2; } else { @@ -129,13 +141,22 @@ public class SmackIntegrationTestFramework { System.exit(exitStatus); } - public SmackIntegrationTestFramework(Configuration configuration) { + public SmackIntegrationTestFramework(Configuration configuration, Class defaultConnectionClass) + throws KeyManagementException, InstantiationException, IllegalAccessException, IllegalArgumentException, + InvocationTargetException, NoSuchAlgorithmException, SmackException, IOException, XMPPException, + InterruptedException { this.config = configuration; + this.defaultConnectionClass = defaultConnectionClass; } - public synchronized TestRunResult run() throws KeyManagementException, NoSuchAlgorithmException, SmackException, - IOException, XMPPException, InterruptedException { + public synchronized TestRunResult run() + throws KeyManagementException, NoSuchAlgorithmException, SmackException, IOException, XMPPException, + InterruptedException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { testRunResult = new TestRunResult(); + + // Create a connection manager *after* we created the testRunId (in testRunResult). + this.connectionManager = new XmppConnectionManager<>(this, defaultConnectionClass); + LOGGER.info("SmackIntegrationTestFramework [" + testRunResult.testRunId + ']' + ": Starting"); if (config.debugger != Configuration.Debugger.none) { // JUL Debugger will not print any information until configured to print log messages of @@ -147,7 +168,7 @@ public class SmackIntegrationTestFramework { if (config.replyTimeout > 0) { SmackConfiguration.setDefaultReplyTimeout(config.replyTimeout); } - if (config.securityMode != SecurityMode.required) { + if (config.securityMode != SecurityMode.required && config.accountRegistration == AccountRegistration.inBandRegistration) { AccountManager.sensitiveOperationOverInsecureConnectionDefault(true); } // TODO print effective configuration @@ -169,6 +190,18 @@ public class SmackIntegrationTestFramework { classes.addAll(inttestClasses); classes.addAll(lowLevelInttestClasses); + { + // Remove all abstract classes. + // TODO: This may be a good candidate for Java stream filtering once Smack is Android API 24 or higher. + Iterator> it = classes.iterator(); + while (it.hasNext()) { + Class clazz = it.next(); + if (Modifier.isAbstract(clazz.getModifiers())) { + it.remove(); + } + } + } + if (classes.isEmpty()) { throw new IllegalStateException("No test classes found"); } @@ -182,9 +215,7 @@ public class SmackIntegrationTestFramework { } finally { // Ensure that the accounts are deleted and disconnected before we continue - disconnectAndMaybeDelete(environment.conOne); - disconnectAndMaybeDelete(environment.conTwo); - disconnectAndMaybeDelete(environment.conThree); + connectionManager.disconnectAndCleanup(); } return testRunResult; @@ -192,10 +223,42 @@ public class SmackIntegrationTestFramework { @SuppressWarnings({"unchecked", "Finally"}) private void runTests(Set> classes) - throws NoResponseException, InterruptedException { + throws InterruptedException, InstantiationException, IllegalAccessException, + IllegalArgumentException, SmackException, IOException, XMPPException { for (Class testClass : classes) { final String testClassName = testClass.getName(); + // TODO: Move the whole "skipping section" below one layer up? + + // Skip pseudo integration tests from src/test + // Although Smack's gradle build files do not state that the 'main' sources classpath also contains the + // 'test' classes. Some IDEs like Eclipse include them. As result, a real integration test run encounters + // pseudo integration tests like the DummySmackIntegrationTest which always throws from src/test. + // It is unclear why this apparently does not happen in the 4.3 branch, one likely cause is + // compile project(path: ":smack-omemo", configuration: "testRuntime") + // in + // smack-integration-test/build.gradle:17 + // added after 4.3 was branched out with + // 1f731f6318785a84b9741280d586a61dc37ecb2e + // Now "gradle integrationTest" appear to be never affected by this, i.e., they are executed with the + // correct classpath. Plain Eclipse, i.e. Smack imported into Eclipse after "gradle eclipse", appear + // to include *all* classes. Which means those runs sooner or later try to execute + // DummySmackIntegrationTest. Eclipse with buildship, the gradle plugin for Eclipse, always excludes + // *all* src/test classes, which means they do not encounter DummySmackIntegrationTest, but this means + // that the "compile project(path: ":smack-omemo", configuration: "testRuntime")" is not respected, + // which leads to + // Exception in thread "main" java.lang.NoClassDefFoundError: org/jivesoftware/smack/test/util/FileTestUtil + // at org.jivesoftware.smackx.ox.OXSecretKeyBackupIntegrationTest.(OXSecretKeyBackupIntegrationTest.java:66) + // See + // - https://github.com/eclipse/buildship/issues/354 (Remove test dependencies from runtime classpath) + // - https://bugs.eclipse.org/bugs/show_bug.cgi?id=482315 (Runtime classpath includes test dependencies) + // - https://discuss.gradle.org/t/main-vs-test-compile-vs-runtime-classpaths-in-eclipse-once-and-for-all-how/17403 + // - https://bugs.eclipse.org/bugs/show_bug.cgi?id=376616 (Scope of dependencies has no effect on Eclipse compilation) + if (!SINTTEST_UNIT_TEST && testClassName.startsWith("org.igniterealtime.smack.inttest.unittest")) { + LOGGER.finer("Skipping integration test '" + testClassName + "' from src/test classpath"); + continue; + } + if (config.enabledTests != null && !isInSet(testClass, config.enabledTests)) { LOGGER.info("Skipping test class " + testClassName + " because it is not enabled"); continue; @@ -206,46 +269,84 @@ public class SmackIntegrationTestFramework { continue; } - TestType testType; - if (AbstractSmackLowLevelIntegrationTest.class.isAssignableFrom(testClass)) { + final Constructor cons; + try { + cons = testClass.getConstructor(SmackIntegrationTestEnvironment.class); + } + catch (NoSuchMethodException | SecurityException e) { + throw new IllegalArgumentException( + "Smack Integration Test class does not declare the correct constructor. Is a public Constructor(SmackIntegrationTestEnvironment) missing?", + e); + } + + final List smackIntegrationTestMethods; + { + Method[] testClassMethods = testClass.getMethods(); + smackIntegrationTestMethods = new ArrayList<>(testClassMethods.length); + for (Method method : testClassMethods) { + if (!method.isAnnotationPresent(SmackIntegrationTest.class)) { + continue; + } + smackIntegrationTestMethods.add(method); + } + } + + if (smackIntegrationTestMethods.isEmpty()) { + LOGGER.warning("No Smack integration test methods found in " + testClass); + continue; + } + + testRunResult.numberOfAvailableTestMethods.addAndGet(smackIntegrationTestMethods.size()); + + final AbstractSmackIntTest test; + try { + test = cons.newInstance(environment); + } + catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + + throwFatalException(cause); + + testRunResult.impossibleTestClasses.put(testClass, cause); + continue; + } + + Class specificLowLevelConnectionClass = null; + final TestType testType; + if (test instanceof AbstractSmackSpecificLowLevelIntegrationTest) { + AbstractSmackSpecificLowLevelIntegrationTest specificLowLevelTest = (AbstractSmackSpecificLowLevelIntegrationTest) test; + specificLowLevelConnectionClass = specificLowLevelTest.getConnectionClass(); + testType = TestType.SpecificLowLevel; + } else if (test instanceof AbstractSmackLowLevelIntegrationTest) { testType = TestType.LowLevel; - } else if (AbstractSmackIntegrationTest.class.isAssignableFrom(testClass)) { + } else if (test instanceof AbstractSmackIntegrationTest) { testType = TestType.Normal; } else { throw new AssertionError(); } - List smackIntegrationTestMethods = new LinkedList<>(); - for (Method method : testClass.getMethods()) { - if (!method.isAnnotationPresent(SmackIntegrationTest.class)) { - continue; - } + + // Verify the method signatures, throw in case a signature is incorrect. + for (Method method : smackIntegrationTestMethods) { Class retClass = method.getReturnType(); if (!retClass.equals(Void.TYPE)) { - LOGGER.warning("SmackIntegrationTest annotation on method that does not return void"); - continue; + throw new IllegalStateException( + "SmackIntegrationTest annotation on" + method + " that does not return void"); } - final Class[] parameterTypes = method.getParameterTypes(); switch (testType) { case Normal: - if (method.getParameterTypes().length > 0) { - LOGGER.warning("SmackIntegrationTest annotaton on method that takes arguments "); - continue; + final Class[] parameterTypes = method.getParameterTypes(); + if (parameterTypes.length > 0) { + throw new IllegalStateException( + "SmackIntegrationTest annotaton on " + method + " that takes arguments "); } break; case LowLevel: - for (Class parameterType : parameterTypes) { - if (!parameterType.isAssignableFrom(XMPPTCPConnection.class)) { - LOGGER.warning("SmackIntegrationTest low-level test method declares parameter that is not of type XMPPTCPConnection"); - } - } + verifyLowLevelTestMethod(method, AbstractXMPPConnection.class); + break; + case SpecificLowLevel: + verifyLowLevelTestMethod(method, specificLowLevelConnectionClass); break; } - smackIntegrationTestMethods.add(method); - } - - if (smackIntegrationTestMethods.isEmpty()) { - LOGGER.warning("No integration test methods found"); - continue; } Iterator it = smackIntegrationTestMethods.iterator(); @@ -271,75 +372,7 @@ public class SmackIntegrationTestFramework { } final int detectedTestMethodsCount = smackIntegrationTestMethods.size(); - testRunResult.numberOfAvailableTests.addAndGet(detectedTestMethodsCount); - testRunResult.numberOfPossibleTests.addAndGet(detectedTestMethodsCount); - - AbstractSmackIntTest test; - switch (testType) { - case Normal: { - Constructor cons; - try { - cons = ((Class) testClass).getConstructor(SmackIntegrationTestEnvironment.class); - } - catch (NoSuchMethodException | SecurityException e) { - LOGGER.log(Level.WARNING, - "Smack Integration Test class could not get constructed (public Con)structor(SmackIntegrationTestEnvironment) missing?)", - e); - continue; - } - - try { - test = cons.newInstance(environment); - } - catch (InvocationTargetException e) { - Throwable cause = e.getCause(); - throwFatalException(cause); - - testRunResult.impossibleTestClasses.put(testClass, cause.getMessage()); - testRunResult.numberOfPossibleTests.addAndGet(-detectedTestMethodsCount); - continue; - } - catch (InstantiationException | IllegalAccessException | IllegalArgumentException e) { - LOGGER.log(Level.WARNING, "todo", e); - continue; - } - } break; - case LowLevel: { - Constructor cons; - try { - cons = ((Class) testClass).getConstructor( - SmackIntegrationTestEnvironment.class); - } - catch (NoSuchMethodException | SecurityException e) { - LOGGER.log(Level.WARNING, - "Smack Integration Test class could not get constructed (public Con)structor(SmackIntegrationTestEnvironment) missing?)", - e); - continue; - } - - try { - test = cons.newInstance(environment); - } - catch (InvocationTargetException e) { - Throwable cause = e.getCause(); - if (cause instanceof TestNotPossibleException) { - testRunResult.impossibleTestClasses.put(testClass, cause.getMessage()); - testRunResult.numberOfPossibleTests.addAndGet(-detectedTestMethodsCount); - } - else { - throwFatalException(cause); - LOGGER.log(Level.WARNING, "Could not construct test class", e); - } - continue; - } - catch (InstantiationException | IllegalAccessException | IllegalArgumentException e) { - LOGGER.log(Level.WARNING, "todo", e); - continue; - } - } break; - default: - throw new AssertionError(); - } + testRunResult.numberOfPossibleTestMethods.addAndGet(detectedTestMethodsCount); try { // Run the @BeforeClass methods (if any) @@ -373,48 +406,36 @@ public class SmackIntegrationTestFramework { } for (Method testMethod : smackIntegrationTestMethods) { - final String testPrefix = testClass.getSimpleName() + '.' - + testMethod.getName() + " (" + testType + "): "; - // Invoke all test methods on the test instance - LOGGER.info(testPrefix + "Start"); - long testStart = System.currentTimeMillis(); - try { + List concreteTests = null; + switch (testType) { + case Normal: { + ConcreteTest.Executor concreteTestExecutor = () -> testMethod.invoke(test); + ConcreteTest concreteTest = new ConcreteTest(testType, testMethod, concreteTestExecutor); + concreteTests = Collections.singletonList(concreteTest); + } + break; + case LowLevel: + case SpecificLowLevel: + LowLevelTestMethod lowLevelTestMethod = new LowLevelTestMethod(testMethod); switch (testType) { - case Normal: - testMethod.invoke(test); - break; case LowLevel: - invokeLowLevel(testMethod, test); + concreteTests = invokeLowLevel(lowLevelTestMethod, (AbstractSmackLowLevelIntegrationTest) test); + break; + case SpecificLowLevel: { + ConcreteTest.Executor concreteTestExecutor = () -> invokeSpecificLowLevel( + lowLevelTestMethod, (AbstractSmackSpecificLowLevelIntegrationTest) test); + ConcreteTest concreteTest = new ConcreteTest(testType, testMethod, concreteTestExecutor); + concreteTests = Collections.singletonList(concreteTest); break; } - LOGGER.info(testPrefix + "Success"); - long testEnd = System.currentTimeMillis(); - testRunResult.successfulTests.add(new SuccessfulTest(testMethod, testStart, testEnd, null)); - } - catch (InvocationTargetException e) { - long testEnd = System.currentTimeMillis(); - Throwable cause = e.getCause(); - if (cause instanceof TestNotPossibleException) { - LOGGER.info(testPrefix + "Not possible"); - testRunResult.impossibleTestMethods.add(new TestNotPossible(testMethod, testStart, testEnd, - null, (TestNotPossibleException) cause)); - continue; + default: + throw new AssertionError(); } - Throwable nonFatalFailureReason; - // junit assert's throw an AssertionError if they fail, those should not be - // thrown up, as it would be done by throwFatalException() - if (cause instanceof AssertionError) { - nonFatalFailureReason = cause; - } else { - nonFatalFailureReason = throwFatalException(cause); - } - // An integration test failed - testRunResult.failedIntegrationTests.add(new FailedTest(testMethod, testStart, testEnd, null, - nonFatalFailureReason)); - LOGGER.log(Level.SEVERE, testPrefix + "Failed", e); + break; } - catch (IllegalArgumentException | IllegalAccessException e) { - throw new AssertionError(e); + + for (ConcreteTest concreteTest : concreteTests) { + runConcreteTest(concreteTest); } } } @@ -452,67 +473,86 @@ public class SmackIntegrationTestFramework { } } - private void invokeLowLevel(Method testMethod, AbstractSmackIntTest test) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException, InterruptedException { - // We have checked before that every parameter, if any, is of type XMPPTCPConnection - final int numberOfConnections = testMethod.getParameterTypes().length; - XMPPTCPConnection[] connections = null; + private void runConcreteTest(ConcreteTest concreteTest) + throws InterruptedException, XMPPException, IOException, SmackException { + LOGGER.info(concreteTest + " Start"); + long testStart = System.currentTimeMillis(); try { - if (numberOfConnections > 0 && !config.isAccountRegistrationPossible()) { - throw new TestNotPossibleException( - "Must create accounts for this test, but it's not enabled"); - } - connections = new XMPPTCPConnection[numberOfConnections]; - for (int i = 0; i < numberOfConnections; ++i) { - connections[i] = getConnectedConnection(environment, i); - } + concreteTest.executor.execute(); + long testEnd = System.currentTimeMillis(); + LOGGER.info(concreteTest + " Success"); + testRunResult.successfulIntegrationTests.add(new SuccessfulTest(concreteTest, testStart, testEnd, null)); } - catch (Exception e) { - if (e instanceof RuntimeException) { - throw (RuntimeException) e; + catch (InvocationTargetException e) { + long testEnd = System.currentTimeMillis(); + Throwable cause = e.getCause(); + if (cause instanceof TestNotPossibleException) { + LOGGER.info(concreteTest + " is not possible"); + testRunResult.impossibleIntegrationTests.add(new TestNotPossible(concreteTest, testStart, testEnd, + null, (TestNotPossibleException) cause)); + return; } - // Behave like this was an InvocationTargetException - throw new InvocationTargetException(e); - } - try { - testMethod.invoke(test, (Object[]) connections); - } - finally { - for (int i = 0; i < numberOfConnections; ++i) { - IntTestUtil.disconnectAndMaybeDelete(connections[i], config); + Throwable nonFatalFailureReason; + // junit assert's throw an AssertionError if they fail, those should not be + // thrown up, as it would be done by throwFatalException() + if (cause instanceof AssertionError) { + nonFatalFailureReason = cause; + } else { + nonFatalFailureReason = throwFatalException(cause); } + // An integration test failed + testRunResult.failedIntegrationTests.add(new FailedTest(concreteTest, testStart, testEnd, null, + nonFatalFailureReason)); + LOGGER.log(Level.SEVERE, concreteTest + " Failed", e); + } + catch (IllegalArgumentException | IllegalAccessException e) { + throw new AssertionError(e); } } - protected void disconnectAndMaybeDelete(XMPPTCPConnection connection) throws InterruptedException { - IntTestUtil.disconnectAndMaybeDelete(connection, config); + private static void verifyLowLevelTestMethod(Method method, + Class connectionClass) { + if (!testMethodParametersIsListOfConnections(method, connectionClass) + && !testMethodParametersVarargsConnections(method, connectionClass)) { + throw new IllegalArgumentException(method + " is not a valid low level test method"); + } } - protected SmackIntegrationTestEnvironment prepareEnvironment() throws SmackException, + private List invokeLowLevel(LowLevelTestMethod lowLevelTestMethod, AbstractSmackLowLevelIntegrationTest test) { + Set> connectionClasses; + if (lowLevelTestMethod.smackIntegrationTestAnnotation.onlyDefaultConnectionType()) { + Class defaultConnectionClass = connectionManager.getDefaultConnectionClass(); + connectionClasses = Collections.singleton(defaultConnectionClass); + } else { + connectionClasses = connectionManager.getConnectionClasses(); + } + + List resultingConcreteTests = new ArrayList<>(connectionClasses.size()); + + for (Class connectionClass : connectionClasses) { + ConcreteTest.Executor executor = () -> lowLevelTestMethod.invoke(test, connectionClass); + ConcreteTest concreteTest = new ConcreteTest(TestType.LowLevel, lowLevelTestMethod.testMethod, executor, connectionClass.getSimpleName()); + resultingConcreteTests.add(concreteTest); + } + + return resultingConcreteTests; + } + + private void invokeSpecificLowLevel(LowLevelTestMethod testMethod, + AbstractSmackSpecificLowLevelIntegrationTest test) + throws IllegalAccessException, IllegalArgumentException, InvocationTargetException, InterruptedException, + SmackException, IOException, XMPPException { + if (testMethod.smackIntegrationTestAnnotation.onlyDefaultConnectionType()) { + throw new IllegalArgumentException("SpecificLowLevelTests must not have set onlyDefaultConnectionType"); + } + Class connectionClass = test.getConnectionClass(); + testMethod.invoke(test, connectionClass); + } + + protected SmackIntegrationTestEnvironment prepareEnvironment() throws SmackException, IOException, XMPPException, InterruptedException, KeyManagementException, - NoSuchAlgorithmException { - XMPPTCPConnection conOne = null; - XMPPTCPConnection conTwo = null; - XMPPTCPConnection conThree = null; - try { - conOne = getConnectedConnectionFor(AccountNum.One); - conTwo = getConnectedConnectionFor(AccountNum.Two); - conThree = getConnectedConnectionFor(AccountNum.Three); - } - catch (Exception e) { - // TODO Reverse the order, i.e. conThree should be disconnected first. - if (conOne != null) { - conOne.disconnect(); - } - if (conTwo != null) { - conTwo.disconnect(); - } - if (conThree != null) { - conThree.disconnect(); - } - throw e; - } - - return new SmackIntegrationTestEnvironment(conOne, conTwo, conThree, testRunResult.testRunId, config); + NoSuchAlgorithmException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { + return connectionManager.prepareEnvironment(); } enum AccountNum { @@ -521,99 +561,14 @@ public class SmackIntegrationTestFramework { Three, } - private static final String USERNAME_PREFIX = "smack-inttest"; - - private XMPPTCPConnection getConnectedConnectionFor(AccountNum accountNum) - throws SmackException, IOException, XMPPException, InterruptedException, - KeyManagementException, NoSuchAlgorithmException { - String middlefix; - String accountUsername; - String accountPassword; - switch (accountNum) { - case One: - accountUsername = config.accountOneUsername; - accountPassword = config.accountOnePassword; - middlefix = "one"; - break; - case Two: - accountUsername = config.accountTwoUsername; - accountPassword = config.accountTwoPassword; - middlefix = "two"; - break; - case Three: - accountUsername = config.accountThreeUsername; - accountPassword = config.accountThreePassword; - middlefix = "three"; - break; - default: - throw new IllegalStateException(); - } - if (StringUtils.isNullOrEmpty(accountUsername)) { - accountUsername = USERNAME_PREFIX + '-' + middlefix + '-' + testRunResult.testRunId; - } - if (StringUtils.isNullOrEmpty(accountPassword)) { - accountPassword = StringUtils.insecureRandomString(16); - } - - XMPPTCPConnectionConfiguration.Builder builder = getConnectionConfigurationBuilder(config); - builder.setUsernameAndPassword(accountUsername, accountPassword) - .setResource(middlefix + '-' + testRunResult.testRunId); - - XMPPTCPConnection connection = new XMPPTCPConnection(builder.build()); - connection.connect(); - if (config.isAccountRegistrationPossible()) { - UsernameAndPassword uap = IntTestUtil.registerAccount(connection, accountUsername, accountPassword, config); - - // TODO is this still required? - // Some servers, e.g. Openfire, do not support a login right after the account was - // created, so disconnect and re-connection the connection first. - connection.disconnect(); - connection.connect(); - - connection.login(uap.username, uap.password); - } else { - connection.login(); - } - - return connection; - } - static XMPPTCPConnectionConfiguration.Builder getConnectionConfigurationBuilder(Configuration config) { XMPPTCPConnectionConfiguration.Builder builder = XMPPTCPConnectionConfiguration.builder(); - if (config.tlsContext != null) { - builder.setCustomSSLContext(config.tlsContext); - } - builder.setSecurityMode(config.securityMode); - builder.setXmppDomain(config.service); - switch (config.debugger) { - case enhanced: - builder.setDebuggerFactory(EnhancedDebugger.Factory.INSTANCE); - break; - case console: - builder.setDebuggerFactory(ConsoleDebugger.Factory.INSTANCE); - break; - case none: - // Nothing to do :). - break; - } + config.configurationApplier.applyConfigurationTo(builder); return builder; } - static XMPPTCPConnection getConnectedConnection(SmackIntegrationTestEnvironment environment, int connectionId) - throws KeyManagementException, NoSuchAlgorithmException, InterruptedException, - SmackException, IOException, XMPPException { - Configuration config = environment.configuration; - XMPPTCPConnectionConfiguration.Builder builder = getConnectionConfigurationBuilder(config); - - XMPPTCPConnection connection = new XMPPTCPConnection(builder.build()); - connection.connect(); - UsernameAndPassword uap = IntTestUtil.registerAccount(connection, environment, connectionId); - connection.login(uap.username, uap.password); - return connection; - } - private static Exception throwFatalException(Throwable e) throws Error, NoResponseException, InterruptedException { if (e instanceof NoResponseException) { @@ -649,14 +604,14 @@ public class SmackIntegrationTestFramework { */ public final String testRunId = StringUtils.insecureRandomString(5).toLowerCase(Locale.US); - private final List successfulTests = Collections.synchronizedList(new LinkedList()); + private final List successfulIntegrationTests = Collections.synchronizedList(new LinkedList()); private final List failedIntegrationTests = Collections.synchronizedList(new LinkedList()); - private final List impossibleTestMethods = Collections.synchronizedList(new LinkedList()); - private final Map, String> impossibleTestClasses = new HashMap<>(); - private final AtomicInteger numberOfAvailableTests = new AtomicInteger(); - private final AtomicInteger numberOfPossibleTests = new AtomicInteger(); + private final List impossibleIntegrationTests = Collections.synchronizedList(new LinkedList()); + private final Map, Throwable> impossibleTestClasses = new HashMap<>(); + private final AtomicInteger numberOfAvailableTestMethods = new AtomicInteger(); + private final AtomicInteger numberOfPossibleTestMethods = new AtomicInteger(); - private TestRunResult() { + TestRunResult() { } public String getTestRunId() { @@ -664,15 +619,15 @@ public class SmackIntegrationTestFramework { } public int getNumberOfAvailableTests() { - return numberOfAvailableTests.get(); + return numberOfAvailableTestMethods.get(); } public int getNumberOfPossibleTests() { - return numberOfPossibleTests.get(); + return numberOfPossibleTestMethods.get(); } public List getSuccessfulTests() { - return Collections.unmodifiableList(successfulTests); + return Collections.unmodifiableList(successfulIntegrationTests); } public List getFailedTests() { @@ -680,11 +635,154 @@ public class SmackIntegrationTestFramework { } public List getNotPossibleTests() { - return Collections.unmodifiableList(impossibleTestMethods); + return Collections.unmodifiableList(impossibleIntegrationTests); } - public Map, String> getImpossibleTestClasses() { + public Map, Throwable> getImpossibleTestClasses() { return Collections.unmodifiableMap(impossibleTestClasses); } } + + static final class ConcreteTest { + private final TestType testType; + private final Method method; + private final Executor executor; + private final String[] subdescriptons; + + private ConcreteTest(TestType testType, Method method, Executor executor, String... subdescriptions) { + this.testType = testType; + this.method = method; + this.executor = executor; + this.subdescriptons = subdescriptions; + } + + private transient String stringCache; + + @Override + public String toString() { + if (stringCache != null) { + return stringCache; + } + + StringBuilder sb = new StringBuilder(); + sb.append(method.getDeclaringClass().getSimpleName()) + .append('.') + .append(method.getName()) + .append(" (") + .append(testType.name()); + final String SUBDESCRIPTION_DELIMITER = ", "; + sb.append(SUBDESCRIPTION_DELIMITER); + + for (String subdescripton : subdescriptons) { + sb.append(subdescripton).append(SUBDESCRIPTION_DELIMITER); + } + sb.setLength(sb.length() - SUBDESCRIPTION_DELIMITER.length()); + sb.append(')'); + + stringCache = sb.toString(); + return stringCache; + } + + private interface Executor { + + /** + * Execute the test. + * + * @throws IllegalAccessException + * @throws InterruptedException + * @throws InvocationTargetException if the reflective invoked test throws an exception. + * @throws XMPPException in case an XMPPException happens when preparing the test. + * @throws IOException in case an IOException happens when preparing the test. + * @throws SmackException in case an SmackException happens when preparing the test. + */ + void execute() throws IllegalAccessException, InterruptedException, InvocationTargetException, + XMPPException, IOException, SmackException; + } + } + + private final class LowLevelTestMethod { + private final Method testMethod; + private final SmackIntegrationTest smackIntegrationTestAnnotation; + private final boolean parameterListOfConnections; + + private LowLevelTestMethod(Method testMethod) { + this.testMethod = testMethod; + + smackIntegrationTestAnnotation = testMethod.getAnnotation(SmackIntegrationTest.class); + assert (smackIntegrationTestAnnotation != null); + parameterListOfConnections = testMethodParametersIsListOfConnections(testMethod); + } + + private void invoke(AbstractSmackLowLevelIntegrationTest test, + Class connectionClass) + throws IllegalAccessException, IllegalArgumentException, InvocationTargetException, + InterruptedException, SmackException, IOException, XMPPException { + final int connectionCount; + if (parameterListOfConnections) { + connectionCount = smackIntegrationTestAnnotation.connectionCount(); + if (connectionCount < 1) { + throw new IllegalArgumentException(testMethod + " is annotated to use less than one connection ('" + + connectionCount + ')'); + } + } else { + connectionCount = testMethod.getParameterCount(); + } + + List connections = connectionManager.constructConnectedConnections( + connectionClass, connectionCount); + + if (parameterListOfConnections) { + testMethod.invoke(test, connections); + } else { + Object[] connectionsArray = new Object[connectionCount]; + for (int i = 0; i < connectionsArray.length; i++) { + connectionsArray[i] = connections.remove(0); + } + testMethod.invoke(test, connectionsArray); + } + } + } + + private static boolean testMethodParametersIsListOfConnections(Method testMethod) { + return testMethodParametersIsListOfConnections(testMethod, AbstractXMPPConnection.class); + } + + static boolean testMethodParametersIsListOfConnections(Method testMethod, Class connectionClass) { + Type[] parameterTypes = testMethod.getGenericParameterTypes(); + if (parameterTypes.length != 1) { + return false; + } + Class soleParameter = testMethod.getParameterTypes()[0]; + if (!Collection.class.isAssignableFrom(soleParameter)) { + return false; + } + + ParameterizedType soleParameterizedType = (ParameterizedType) parameterTypes[0]; + Type[] actualTypeArguments = soleParameterizedType.getActualTypeArguments(); + if (actualTypeArguments.length != 1) { + return false; + } + + Type soleActualTypeArgument = actualTypeArguments[0]; + if (!(soleActualTypeArgument instanceof Class)) { + return false; + } + Class soleActualTypeArgumentAsClass = (Class) soleActualTypeArgument; + if (!connectionClass.isAssignableFrom(soleActualTypeArgumentAsClass)) { + return false; + } + + return true; + } + + static boolean testMethodParametersVarargsConnections(Method testMethod, Class connectionClass) { + Class[] parameterTypes = testMethod.getParameterTypes(); + for (Class parameterType : parameterTypes) { + if (!parameterType.isAssignableFrom(connectionClass)) { + return false; + } + } + + return true; + } } diff --git a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/SuccessfulTest.java b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/SuccessfulTest.java index c8d96c77f..3d3c19625 100644 --- a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/SuccessfulTest.java +++ b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/SuccessfulTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015 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. @@ -16,13 +16,12 @@ */ package org.igniterealtime.smack.inttest; -import java.lang.reflect.Method; import java.util.List; public class SuccessfulTest extends TestResult { - public SuccessfulTest(Method testMethod, long startTime, long endTime, List logMessages) { - super(testMethod, startTime, endTime, logMessages); + public SuccessfulTest(SmackIntegrationTestFramework.ConcreteTest concreteTest, long startTime, long endTime, List logMessages) { + super(concreteTest, startTime, endTime, logMessages); } } diff --git a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/TestNotPossible.java b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/TestNotPossible.java index 9783a5e49..a3981e2e9 100644 --- a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/TestNotPossible.java +++ b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/TestNotPossible.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015 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. @@ -16,16 +16,15 @@ */ package org.igniterealtime.smack.inttest; -import java.lang.reflect.Method; import java.util.List; public class TestNotPossible extends TestResult { public final TestNotPossibleException testNotPossibleException; - public TestNotPossible(Method testMethod, long startTime, long endTime, List logMessages, + public TestNotPossible(SmackIntegrationTestFramework.ConcreteTest concreteTest, long startTime, long endTime, List logMessages, TestNotPossibleException testNotPossibleException) { - super(testMethod, startTime, endTime, logMessages); + super(concreteTest, startTime, endTime, logMessages); this.testNotPossibleException = testNotPossibleException; } } diff --git a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/TestResult.java b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/TestResult.java index 86267953b..b3626dde5 100644 --- a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/TestResult.java +++ b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/TestResult.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015 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. @@ -16,19 +16,18 @@ */ package org.igniterealtime.smack.inttest; -import java.lang.reflect.Method; import java.util.List; public abstract class TestResult { - public final Method testMethod; + public final SmackIntegrationTestFramework.ConcreteTest concreteTest; public final long startTime; public final long endTime; public final long duration; public final List logMessages; - public TestResult(Method testMethod, long startTime, long endTime, List logMessages) { - this.testMethod = testMethod; + public TestResult(SmackIntegrationTestFramework.ConcreteTest concreteTest, long startTime, long endTime, List logMessages) { + this.concreteTest = concreteTest; assert (endTime >= startTime); this.startTime = startTime; this.endTime = endTime; diff --git a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/XmppConnectionDescriptor.java b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/XmppConnectionDescriptor.java new file mode 100644 index 000000000..5ff5fb369 --- /dev/null +++ b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/XmppConnectionDescriptor.java @@ -0,0 +1,104 @@ +/** + * + * Copyright 2018-2019 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.igniterealtime.smack.inttest; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.jivesoftware.smack.AbstractXMPPConnection; +import org.jivesoftware.smack.ConnectionConfiguration; +import org.jivesoftware.smack.XMPPConnection; + +public class XmppConnectionDescriptor> { + + private final Class connectionClass; + private final Class connectionConfigurationClass; + + private final Constructor connectionConstructor; + private final Method builderMethod; + + public XmppConnectionDescriptor(Class connectionClass, Class connectionConfigurationClass) + throws ClassNotFoundException, NoSuchMethodException, SecurityException { + this.connectionClass = connectionClass; + this.connectionConfigurationClass = connectionConfigurationClass; + + this.connectionConstructor = getConstructor(connectionClass, connectionConfigurationClass); + this.builderMethod = getBuilderMethod(connectionConfigurationClass); + } + + public C construct(Configuration sinttestConfiguration) + throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { + return construct(sinttestConfiguration, Collections.emptyList()); + } + + public C construct(Configuration sinttestConfiguration, + ConnectionConfigurationBuilderApplier... customConnectionConfigurationAppliers) + throws InstantiationException, IllegalAccessException, IllegalArgumentException, + InvocationTargetException { + List customConnectionConfigurationAppliersList = new ArrayList( + Arrays.asList(customConnectionConfigurationAppliers)); + return construct(sinttestConfiguration, customConnectionConfigurationAppliersList); + } + + public C construct(Configuration sinttestConfiguration, + Collection customConnectionConfigurationAppliers) + throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { + CCB connectionConfigurationBuilder = getNewBuilder(); + for (ConnectionConfigurationBuilderApplier customConnectionConfigurationApplier : customConnectionConfigurationAppliers) { + customConnectionConfigurationApplier.applyConfigurationTo(connectionConfigurationBuilder); + } + sinttestConfiguration.configurationApplier.applyConfigurationTo(connectionConfigurationBuilder); + ConnectionConfiguration connectionConfiguration = connectionConfigurationBuilder.build(); + CC concreteConnectionConfiguration = connectionConfigurationClass.cast(connectionConfiguration); + return connectionConstructor.newInstance(concreteConnectionConfiguration); + } + + @SuppressWarnings("unchecked") + public CCB getNewBuilder() throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { + return (CCB) builderMethod.invoke(null); + } + + public Class getConnectionClass() { + return connectionClass; + } + + private static Constructor getConstructor(Class connectionClass, + Class connectionConfigurationClass) + throws NoSuchMethodException, SecurityException { + return connectionClass.getConstructor(connectionConfigurationClass); + } + + private static Method getBuilderMethod(Class connectionConfigurationClass) + throws NoSuchMethodException, SecurityException { + Method builderMethod = connectionConfigurationClass.getMethod("builder"); + if (!Modifier.isStatic(builderMethod.getModifiers())) { + throw new IllegalArgumentException(); + } + Class returnType = builderMethod.getReturnType(); + if (!ConnectionConfiguration.Builder.class.isAssignableFrom(returnType)) { + throw new IllegalArgumentException(); + } + return builderMethod; + } +} 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 new file mode 100644 index 000000000..4bc28f4b5 --- /dev/null +++ b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/XmppConnectionManager.java @@ -0,0 +1,441 @@ +/** + * + * Copyright 2018-2019 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.igniterealtime.smack.inttest; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.jivesoftware.smack.AbstractXMPPConnection; +import org.jivesoftware.smack.ConnectionConfiguration; +import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.SmackException.NoResponseException; +import org.jivesoftware.smack.SmackException.NotConnectedException; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.XMPPException.XMPPErrorException; +import org.jivesoftware.smack.tcp.XMPPTCPConnection; +import org.jivesoftware.smack.tcp.XMPPTCPConnectionConfiguration; +import org.jivesoftware.smack.tcp.XmppNioTcpConnection; +import org.jivesoftware.smack.util.MultiMap; +import org.jivesoftware.smack.util.StringUtils; + +import org.jivesoftware.smackx.admin.ServiceAdministrationManager; +import org.jivesoftware.smackx.iqregister.AccountManager; + +import org.igniterealtime.smack.inttest.Configuration.AccountRegistration; +import org.igniterealtime.smack.inttest.SmackIntegrationTestFramework.AccountNum; +import org.jxmpp.jid.EntityBareJid; +import org.jxmpp.jid.impl.JidCreate; +import org.jxmpp.jid.parts.Localpart; +import org.jxmpp.stringprep.XmppStringprepException; + +public class XmppConnectionManager { + + private static final Logger LOGGER = Logger.getLogger(XmppConnectionManager.class.getName()); + + private static final Map, XmppConnectionDescriptor>> CONNECTION_DESCRIPTORS = new ConcurrentHashMap<>(); + + static { + try { + addConnectionDescriptor(XmppNioTcpConnection.class, XMPPTCPConnectionConfiguration.class); + addConnectionDescriptor(XMPPTCPConnection.class, XMPPTCPConnectionConfiguration.class); + } catch (ClassNotFoundException | NoSuchMethodException | SecurityException e) { + throw new AssertionError(e); + } + } + + public static void addConnectionDescriptor(Class connectionClass, + Class connectionConfigurationClass) throws ClassNotFoundException, NoSuchMethodException, SecurityException { + XmppConnectionDescriptor> connectionDescriptor = new XmppConnectionDescriptor<>( + connectionClass, connectionConfigurationClass); + addConnectionDescriptor(connectionDescriptor); + } + + public static void addConnectionDescriptor( + XmppConnectionDescriptor> connectionDescriptor) { + Class connectionClass = connectionDescriptor.getConnectionClass(); + CONNECTION_DESCRIPTORS.put(connectionClass, connectionDescriptor); + } + + public static void removeConnectionDescriptor(Class connectionClass) { + CONNECTION_DESCRIPTORS.remove(connectionClass); + } + + private final XmppConnectionDescriptor> defaultConnectionDescriptor; + + private final Map, XmppConnectionDescriptor>> connectionDescriptors = new HashMap<>( + CONNECTION_DESCRIPTORS.size()); + + private final SmackIntegrationTestFramework sinttestFramework; + private final Configuration sinttestConfiguration; + private final String testRunId; + + private final DC accountRegistrationConnection; + private final ServiceAdministrationManager adminManager; + private final AccountManager accountManager; + + /** + * One of the three main connections. The type of the main connections is the default connection type. + */ + DC conOne, conTwo, conThree; + + /** + * A pool of authenticated and free to use connections. + */ + private final MultiMap, AbstractXMPPConnection> connectionPool = new MultiMap<>(); + + /** + * A list of all ever created connections. + */ + private final List connections = new ArrayList<>(); + + @SuppressWarnings("unchecked") + XmppConnectionManager(SmackIntegrationTestFramework sinttestFramework, + Class defaultConnectionClass) + throws SmackException, IOException, XMPPException, InterruptedException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { + this.sinttestFramework = sinttestFramework; + this.sinttestConfiguration = sinttestFramework.config; + this.testRunId = sinttestFramework.testRunResult.testRunId; + + connectionDescriptors.putAll(CONNECTION_DESCRIPTORS); + + defaultConnectionDescriptor = (XmppConnectionDescriptor>) connectionDescriptors.get( + defaultConnectionClass); + if (defaultConnectionDescriptor == null) { + throw new IllegalArgumentException("Could not find a connection descriptor for " + defaultConnectionClass); + } + + switch (sinttestConfiguration.accountRegistration) { + case serviceAdministration: + case inBandRegistration: + accountRegistrationConnection = defaultConnectionDescriptor.construct(sinttestConfiguration); + accountRegistrationConnection.connect(); + accountRegistrationConnection.login(sinttestConfiguration.adminAccountUsername, + sinttestConfiguration.adminAccountPassword); + + if (sinttestConfiguration.accountRegistration == AccountRegistration.inBandRegistration) { + + adminManager = null; + accountManager = AccountManager.getInstance(accountRegistrationConnection); + } else { + adminManager = ServiceAdministrationManager.getInstanceFor(accountRegistrationConnection); + accountManager = null; + } + break; + case disabled: + accountRegistrationConnection = null; + adminManager = null; + accountManager = null; + break; + default: + throw new AssertionError(); + } + } + + SmackIntegrationTestEnvironment prepareEnvironment() throws KeyManagementException, NoSuchAlgorithmException, + InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, + SmackException, IOException, XMPPException, InterruptedException { + prepareMainConnections(); + return new SmackIntegrationTestEnvironment(conOne, conTwo, conThree, + sinttestFramework.testRunResult.testRunId, sinttestConfiguration, this); + } + + private void prepareMainConnections() throws KeyManagementException, NoSuchAlgorithmException, InstantiationException, + IllegalAccessException, IllegalArgumentException, InvocationTargetException, SmackException, IOException, + XMPPException, InterruptedException { + final int mainAccountCount = AccountNum.values().length; + List connections = new ArrayList<>(mainAccountCount); + for (AccountNum mainAccountNum : AccountNum.values()) { + DC mainConnection = getConnectedMainConnectionFor(mainAccountNum); + connections.add(mainConnection); + } + conOne = connections.get(0); + conTwo = connections.get(1); + conThree = connections.get(2); + } + + public Class getDefaultConnectionClass() { + return defaultConnectionDescriptor.getConnectionClass(); + } + + public Set> getConnectionClasses() { + return Collections.unmodifiableSet(connectionDescriptors.keySet()); + } + + @SuppressWarnings("unchecked") + public XmppConnectionDescriptor> getConnectionDescriptorFor( + Class connectionClass) { + return (XmppConnectionDescriptor>) connectionDescriptors.get( + connectionClass); + } + + void disconnectAndCleanup() throws InterruptedException { + int successfullyDeletedAccountsCount = 0; + for (AbstractXMPPConnection connection : connections) { + if (sinttestConfiguration.accountRegistration == AccountRegistration.inBandRegistration) { + // Note that we use the account manager from the to-be-deleted connection. + AccountManager accountManager = AccountManager.getInstance(connection); + try { + accountManager.deleteAccount(); + successfullyDeletedAccountsCount++; + } catch (NoResponseException | XMPPErrorException | NotConnectedException e) { + LOGGER.log(Level.WARNING, "Could not delete dynamically registered account", e); + } + } + + connection.disconnect(); + + if (sinttestConfiguration.accountRegistration == AccountRegistration.serviceAdministration) { + String username = connection.getConfiguration().getUsername().toString(); + Localpart usernameAsLocalpart; + try { + usernameAsLocalpart = Localpart.from(username); + } catch (XmppStringprepException e) { + throw new AssertionError(e); + } + + EntityBareJid connectionAddress = JidCreate.entityBareFrom(usernameAsLocalpart, sinttestConfiguration.service); + + try { + adminManager.deleteUser(connectionAddress); + successfullyDeletedAccountsCount++; + } catch (NoResponseException | XMPPErrorException | NotConnectedException e) { + LOGGER.log(Level.WARNING, "Could not delete dynamically registered account", e); + } + } + } + + if (sinttestConfiguration.isAccountRegistrationPossible()) { + int unsuccessfullyDeletedAccountsCount = connections.size() - successfullyDeletedAccountsCount; + if (unsuccessfullyDeletedAccountsCount == 0) { + LOGGER.info("Successsfully deleted all created accounts โœ”"); + } else { + LOGGER.warning("Could not delete all created accounts, " + unsuccessfullyDeletedAccountsCount + " remainaing"); + } + } + + connections.clear(); + } + + + private static final String USERNAME_PREFIX = "smack-inttest"; + + private DC getConnectedMainConnectionFor(AccountNum accountNum) throws SmackException, IOException, XMPPException, + InterruptedException, KeyManagementException, NoSuchAlgorithmException, InstantiationException, + IllegalAccessException, IllegalArgumentException, InvocationTargetException { + String middlefix; + String accountUsername; + String accountPassword; + switch (accountNum) { + case One: + accountUsername = sinttestConfiguration.accountOneUsername; + accountPassword = sinttestConfiguration.accountOnePassword; + middlefix = "one"; + break; + case Two: + accountUsername = sinttestConfiguration.accountTwoUsername; + accountPassword = sinttestConfiguration.accountTwoPassword; + middlefix = "two"; + break; + case Three: + accountUsername = sinttestConfiguration.accountThreeUsername; + accountPassword = sinttestConfiguration.accountThreePassword; + middlefix = "three"; + break; + default: + throw new IllegalStateException(); + } + + // Note that it is perfectly fine for account(Username|Password) to be 'null' at this point. + final String finalAccountUsername = StringUtils.isNullOrEmpty(accountUsername) ? USERNAME_PREFIX + '-' + middlefix + '-' + testRunId : accountUsername; + final String finalAccountPassword = StringUtils.isNullOrEmpty(accountPassword) ? StringUtils.insecureRandomString(16) : accountPassword; + + if (sinttestConfiguration.isAccountRegistrationPossible()) { + registerAccount(finalAccountUsername, finalAccountPassword); + } + + DC mainConnection = defaultConnectionDescriptor.construct(sinttestConfiguration, (builder) -> { + try { + builder.setUsernameAndPassword(finalAccountUsername, finalAccountPassword) + .setResource(middlefix + '-' + testRunId); + } catch (XmppStringprepException e) { + throw new IllegalArgumentException(e); + } + }); + + connections.add(mainConnection); + + mainConnection.connect(); + mainConnection.login(); + + return mainConnection; + } + + private void registerAccount(String username, String password) throws NoResponseException, XMPPErrorException, + NotConnectedException, InterruptedException, XmppStringprepException { + if (accountRegistrationConnection == null) { + throw new IllegalStateException("Account registration not configured"); + } + + switch (sinttestConfiguration.accountRegistration) { + case serviceAdministration: + EntityBareJid userJid = JidCreate.entityBareFrom(Localpart.from(username), + accountRegistrationConnection.getXMPPServiceDomain()); + adminManager.addUser(userJid, password); + break; + case inBandRegistration: + if (!accountManager.supportsAccountCreation()) { + throw new UnsupportedOperationException("Account creation/registation is not supported"); + } + Set requiredAttributes = accountManager.getAccountAttributes(); + if (requiredAttributes.size() > 4) { + throw new IllegalStateException("Unkown required attributes"); + } + Map additionalAttributes = new HashMap<>(); + additionalAttributes.put("name", "Smack Integration Test"); + additionalAttributes.put("email", "flow@igniterealtime.org"); + Localpart usernameLocalpart = Localpart.from(username); + accountManager.createAccount(usernameLocalpart, password, additionalAttributes); + break; + case disabled: + throw new IllegalStateException("Account creation no possible"); + } + } + + List constructConnectedConnections(Class connectionClass, int count) + throws InterruptedException, SmackException, IOException, XMPPException { + List connections = new ArrayList<>(count); + + synchronized (connectionPool) { + @SuppressWarnings("unchecked") + List pooledConnections = (List) connectionPool.getAll(connectionClass); + while (count > 0 && !pooledConnections.isEmpty()) { + C connection = pooledConnections.remove(pooledConnections.size() - 1); + connections.add(connection); + count--; + } + } + + @SuppressWarnings("unchecked") + XmppConnectionDescriptor> connectionDescriptor = (XmppConnectionDescriptor>) connectionDescriptors + .get(connectionClass); + for (int i = 0; i < count; i++) { + C connection = constructConnectedConnection(connectionDescriptor); + connections.add(connection); + } + + return connections; + } + + private C constructConnectedConnection( + XmppConnectionDescriptor> connectionDescriptor) + throws InterruptedException, SmackException, IOException, XMPPException { + C connection = constructConnection(connectionDescriptor, null); + + connection.connect(); + connection.login(); + + return connection; + } + + DC constructConnection() + throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { + return constructConnection(defaultConnectionDescriptor); + } + + C constructConnection( + XmppConnectionDescriptor> connectionDescriptor) + throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { + return constructConnection(connectionDescriptor, null); + } + + private C constructConnection( + XmppConnectionDescriptor> connectionDescriptor, + Collection customConnectionConfigurationAppliers) + throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { + String username = "sinttest-" + testRunId + '-' + (connections.size() + 1); + String password = StringUtils.randomString(24); + + return constructConnection(username, password, connectionDescriptor, customConnectionConfigurationAppliers); + } + + private C constructConnection(final String username, final String password, + XmppConnectionDescriptor> connectionDescriptor, + Collection customConnectionConfigurationAppliers) + throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { + try { + registerAccount(username, password); + } catch (XmppStringprepException e) { + throw new IllegalArgumentException(e); + } + + ConnectionConfigurationBuilderApplier usernameAndPasswordApplier = (configurationBuilder) -> { + configurationBuilder.setUsernameAndPassword(username, password); + }; + + if (customConnectionConfigurationAppliers == null) { + customConnectionConfigurationAppliers = Collections.singleton(usernameAndPasswordApplier); + } else { + customConnectionConfigurationAppliers.add(usernameAndPasswordApplier); + } + + C connection; + try { + connection = connectionDescriptor.construct(sinttestConfiguration, customConnectionConfigurationAppliers); + } catch (InstantiationException | IllegalAccessException | IllegalArgumentException + | InvocationTargetException e) { + throw new IllegalStateException(e); + } + + connections.add(connection); + + return connection; + } + + void recycle(Collection connections) { + for (AbstractXMPPConnection connection : connections) { + recycle(connection); + } + } + + void recycle(AbstractXMPPConnection connection) { + Class connectionClass = connection.getClass(); + if (!connectionDescriptors.containsKey(connectionClass)) { + throw new IllegalStateException("Attempt to recycle unknown connection of class '" + connectionClass + "'"); + } + + if (connection.isAuthenticated()) { + synchronized (connectionPool) { + connectionPool.put(connectionClass, connection); + } + } + // 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/ChatTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smack/ChatTest.java index 43fc3db14..b206fdb5b 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smack/ChatTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smack/ChatTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015 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. @@ -48,7 +48,7 @@ public class ChatTest extends AbstractSmackIntegrationTest { private boolean invoked; @SuppressWarnings("deprecation") - public ChatTest(SmackIntegrationTestEnvironment environment) { + public ChatTest(SmackIntegrationTestEnvironment environment) { super(environment); chatManagerOne = org.jivesoftware.smack.chat.ChatManager.getInstanceFor(conOne); } 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 f7a91b0c0..0c44c42f4 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 @@ -1,6 +1,6 @@ /** * - * Copyright 2015 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. @@ -25,8 +25,6 @@ import java.security.NoSuchAlgorithmException; import org.jivesoftware.smack.sasl.SASLError; import org.jivesoftware.smack.sasl.SASLErrorException; -import org.jivesoftware.smack.tcp.XMPPTCPConnection; -import org.jivesoftware.smack.tcp.XMPPTCPConnectionConfiguration; import org.jivesoftware.smack.util.StringUtils; import org.igniterealtime.smack.inttest.AbstractSmackLowLevelIntegrationTest; @@ -35,7 +33,7 @@ import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; public class LoginIntegrationTest extends AbstractSmackLowLevelIntegrationTest { - public LoginIntegrationTest(SmackIntegrationTestEnvironment environment) { + public LoginIntegrationTest(SmackIntegrationTestEnvironment environment) { super(environment); } @@ -54,14 +52,13 @@ public class LoginIntegrationTest extends AbstractSmackLowLevelIntegrationTest { public void testInvalidLogin() throws SmackException, IOException, XMPPException, InterruptedException, KeyManagementException, NoSuchAlgorithmException { final String nonExistentUserString = StringUtils.insecureRandomString(24); - XMPPTCPConnectionConfiguration conf = getConnectionConfiguration().setUsernameAndPassword( - nonExistentUserString, "invalidPassword").build(); + final String invalidPassword = "invalidPassword"; - XMPPTCPConnection connection = new XMPPTCPConnection(conf); + AbstractXMPPConnection connection = getUnconnectedConnection(); connection.connect(); try { - connection.login(); + connection.login(nonExistentUserString, invalidPassword); fail("Exception expected"); } catch (SASLErrorException e) { diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smack/StreamManagementTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smack/StreamManagementTest.java index ee36306c9..999f14684 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smack/StreamManagementTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smack/StreamManagementTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015 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. @@ -28,23 +28,20 @@ import org.jivesoftware.smack.filter.MessageWithBodiesFilter; import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.tcp.XMPPTCPConnection; -import org.igniterealtime.smack.inttest.AbstractSmackLowLevelIntegrationTest; +import org.igniterealtime.smack.inttest.AbstractSmackSpecificLowLevelIntegrationTest; import org.igniterealtime.smack.inttest.SmackIntegrationTest; import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; import org.igniterealtime.smack.inttest.TestNotPossibleException; -public class StreamManagementTest extends AbstractSmackLowLevelIntegrationTest { +public class StreamManagementTest extends AbstractSmackSpecificLowLevelIntegrationTest { - public StreamManagementTest(SmackIntegrationTestEnvironment environment) throws Exception { - super(environment); - performCheck(new ConnectionCallback() { - @Override - public void connectionCallback(XMPPTCPConnection connection) throws Exception { - if (!connection.isSmAvailable()) { - throw new TestNotPossibleException("XEP-198: Stream Mangement not supported by service"); - } - } - }); + public StreamManagementTest(SmackIntegrationTestEnvironment environment) throws Exception { + super(environment, XMPPTCPConnection.class); + XMPPTCPConnection connection = getSpecificUnconnectedConnection(); + connection.connect().login(); + if (!connection.isSmAvailable()) { + throw new TestNotPossibleException("XEP-198: Stream Mangement not supported by service"); + } } @SmackIntegrationTest 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 bb6e1a1f7..8aa741ecb 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 @@ -1,6 +1,6 @@ /** * - * Copyright 2015-2017 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. @@ -20,25 +20,23 @@ import static org.junit.Assert.assertTrue; import java.lang.reflect.Field; -import org.jivesoftware.smack.tcp.XMPPTCPConnection; - import org.igniterealtime.smack.inttest.AbstractSmackLowLevelIntegrationTest; import org.igniterealtime.smack.inttest.SmackIntegrationTest; import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; public class WaitForClosingStreamElementTest extends AbstractSmackLowLevelIntegrationTest { - public WaitForClosingStreamElementTest(SmackIntegrationTestEnvironment environment) { + public WaitForClosingStreamElementTest(SmackIntegrationTestEnvironment environment) { super(environment); } @SmackIntegrationTest - public void waitForClosingStreamElementTest(XMPPTCPConnection connection) + public void waitForClosingStreamElementTest(AbstractXMPPConnection connection) throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException { connection.disconnect(); - Field closingStreamReceivedField = connection.getClass().getDeclaredField("closingStreamReceived"); + Field closingStreamReceivedField = AbstractXMPPConnection.class.getDeclaredField("closingStreamReceived"); closingStreamReceivedField.setAccessible(true); SynchronizationPoint closingStreamReceived = (SynchronizationPoint) closingStreamReceivedField.get(connection); Exception failureException = closingStreamReceived.getFailureException(); diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smack/XmppConnectionIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smack/XmppConnectionIntegrationTest.java new file mode 100644 index 000000000..8ca3c8498 --- /dev/null +++ b/smack-integration-test/src/main/java/org/jivesoftware/smack/XmppConnectionIntegrationTest.java @@ -0,0 +1,67 @@ +/** + * + * Copyright 2018-2019 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.List; +import java.util.logging.Level; + +import org.jivesoftware.smack.tcp.XmppNioTcpConnection; + +import org.igniterealtime.smack.XmppConnectionStressTest; +import org.igniterealtime.smack.XmppConnectionStressTest.StressTestFailedException.ErrorsWhileSendingOrReceivingException; +import org.igniterealtime.smack.XmppConnectionStressTest.StressTestFailedException.NotAllMessagesReceivedException; +import org.igniterealtime.smack.inttest.AbstractSmackLowLevelIntegrationTest; +import org.igniterealtime.smack.inttest.SmackIntegrationTest; +import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; + +public class XmppConnectionIntegrationTest extends AbstractSmackLowLevelIntegrationTest { + + public XmppConnectionIntegrationTest(SmackIntegrationTestEnvironment environment) { + super(environment); + } + + @SmackIntegrationTest(connectionCount = 4) + public void allToAllMessageSendTest(List connections) + throws InterruptedException, NotAllMessagesReceivedException, ErrorsWhileSendingOrReceivingException { + final long seed = 42; + final int messagesPerConnection = 3; // 100 + final int maxPayloadChunkSize = 16; // 512 + final int maxPayloadChunks = 4; // 32 + final boolean intermixMessages = false; // true + + XmppConnectionStressTest.Configuration stressTestConfiguration = new XmppConnectionStressTest.Configuration( + seed, messagesPerConnection, maxPayloadChunkSize, maxPayloadChunks, intermixMessages); + + XmppConnectionStressTest stressTest = new XmppConnectionStressTest(stressTestConfiguration); + + stressTest.run(connections, timeout); + + final Level connectionStatsLogLevel = Level.FINE; + if (LOGGER.isLoggable(connectionStatsLogLevel)) { + if (connections.get(0) instanceof XmppNioTcpConnection) { + for (XMPPConnection connection : connections) { + XmppNioTcpConnection xmppNioTcpConnection = (XmppNioTcpConnection) connection; + XmppNioTcpConnection.Stats stats = xmppNioTcpConnection.getStats(); + LOGGER.log(connectionStatsLogLevel, + "Connections stats for " + xmppNioTcpConnection + ":\n{}", + stats); + } + } + } + } + +} diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smack/chat2/AbstractChatIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smack/chat2/AbstractChatIntegrationTest.java index 0d174827b..68db46c5a 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smack/chat2/AbstractChatIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smack/chat2/AbstractChatIntegrationTest.java @@ -25,7 +25,7 @@ public abstract class AbstractChatIntegrationTest extends AbstractSmackIntegrati protected final ChatManager chatManagerTwo; protected final ChatManager chatManagerThree; - protected AbstractChatIntegrationTest(SmackIntegrationTestEnvironment environment) { + protected AbstractChatIntegrationTest(SmackIntegrationTestEnvironment environment) { super(environment); chatManagerOne = ChatManager.getInstanceFor(conOne); chatManagerTwo = ChatManager.getInstanceFor(conTwo); diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smack/chat2/IncomingMessageListenerIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smack/chat2/IncomingMessageListenerIntegrationTest.java index ca76ab280..b031be9d2 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smack/chat2/IncomingMessageListenerIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smack/chat2/IncomingMessageListenerIntegrationTest.java @@ -26,7 +26,7 @@ import org.jxmpp.jid.EntityBareJid; public class IncomingMessageListenerIntegrationTest extends AbstractChatIntegrationTest { - public IncomingMessageListenerIntegrationTest(SmackIntegrationTestEnvironment environment) { + public IncomingMessageListenerIntegrationTest(SmackIntegrationTestEnvironment environment) { super(environment); } diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smack/chat2/OutgoingMessageListenerIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smack/chat2/OutgoingMessageListenerIntegrationTest.java index a9221de0c..6ea68d7f4 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smack/chat2/OutgoingMessageListenerIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smack/chat2/OutgoingMessageListenerIntegrationTest.java @@ -26,7 +26,7 @@ import org.jxmpp.jid.EntityBareJid; public class OutgoingMessageListenerIntegrationTest extends AbstractChatIntegrationTest { - public OutgoingMessageListenerIntegrationTest(SmackIntegrationTestEnvironment environment) { + public OutgoingMessageListenerIntegrationTest(SmackIntegrationTestEnvironment environment) { super(environment); } diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smack/roster/LowLevelRosterIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smack/roster/LowLevelRosterIntegrationTest.java index 04e2cdc18..fac15a6dc 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smack/roster/LowLevelRosterIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smack/roster/LowLevelRosterIntegrationTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2016-2018 Florian Schmaus + * Copyright 2016-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. @@ -18,8 +18,8 @@ package org.jivesoftware.smack.roster; import java.util.concurrent.TimeoutException; +import org.jivesoftware.smack.AbstractXMPPConnection; import org.jivesoftware.smack.packet.Presence; -import org.jivesoftware.smack.tcp.XMPPTCPConnection; import org.igniterealtime.smack.inttest.AbstractSmackLowLevelIntegrationTest; import org.igniterealtime.smack.inttest.SmackIntegrationTest; @@ -30,12 +30,13 @@ import org.jxmpp.jid.FullJid; public class LowLevelRosterIntegrationTest extends AbstractSmackLowLevelIntegrationTest { - public LowLevelRosterIntegrationTest(SmackIntegrationTestEnvironment environment) { + public LowLevelRosterIntegrationTest(SmackIntegrationTestEnvironment environment) { super(environment); } @SmackIntegrationTest - public void testPresenceEventListenersOffline(final XMPPTCPConnection conOne, final XMPPTCPConnection conTwo) throws TimeoutException, Exception { + public void testPresenceEventListenersOffline(final AbstractXMPPConnection conOne, + final AbstractXMPPConnection conTwo) throws TimeoutException, Exception { IntegrationTestRosterUtil.ensureBothAccountsAreNotInEachOthersRoster(conOne, conTwo); final Roster rosterOne = Roster.getInstanceFor(conOne); diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smack/roster/RosterIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smack/roster/RosterIntegrationTest.java index fafa776d3..77bd1d8bb 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smack/roster/RosterIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smack/roster/RosterIntegrationTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015-2018 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. @@ -38,7 +38,7 @@ public class RosterIntegrationTest extends AbstractSmackIntegrationTest { private final Roster rosterOne; private final Roster rosterTwo; - public RosterIntegrationTest(SmackIntegrationTestEnvironment environment) { + public RosterIntegrationTest(SmackIntegrationTestEnvironment environment) { super(environment); rosterOne = Roster.getInstanceFor(conOne); rosterTwo = Roster.getInstanceFor(conTwo); diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smack/tcp/XmppNioTcpConnectionLowLevelIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smack/tcp/XmppNioTcpConnectionLowLevelIntegrationTest.java new file mode 100644 index 000000000..f42f04ced --- /dev/null +++ b/smack-integration-test/src/main/java/org/jivesoftware/smack/tcp/XmppNioTcpConnectionLowLevelIntegrationTest.java @@ -0,0 +1,46 @@ +/** + * + * Copyright 2018-2019 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.tcp; + +import java.io.IOException; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; + +import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.XMPPException; + +import org.igniterealtime.smack.inttest.AbstractSmackSpecificLowLevelIntegrationTest; +import org.igniterealtime.smack.inttest.SmackIntegrationTest; +import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; + +public class XmppNioTcpConnectionLowLevelIntegrationTest extends AbstractSmackSpecificLowLevelIntegrationTest { + + public XmppNioTcpConnectionLowLevelIntegrationTest(SmackIntegrationTestEnvironment environment) { + super(environment, XmppNioTcpConnection.class); + } + + @SmackIntegrationTest + public void testDisconnectAfterConnect() throws KeyManagementException, NoSuchAlgorithmException, SmackException, + IOException, XMPPException, InterruptedException { + XmppNioTcpConnection connection = getSpecificUnconnectedConnection(); + + connection.connect(); + + connection.disconnect(); + } + +} diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smack/tcp/package-info.java b/smack-integration-test/src/main/java/org/jivesoftware/smack/tcp/package-info.java new file mode 100644 index 000000000..9d2ac84f5 --- /dev/null +++ b/smack-integration-test/src/main/java/org/jivesoftware/smack/tcp/package-info.java @@ -0,0 +1,21 @@ +/** + * + * 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. + * 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. + */ + +/** + * TCP-IP related classes for Smack. + */ +package org.jivesoftware.smack.tcp; diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/caps/EntityCapsTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/caps/EntityCapsTest.java index 077adc291..17520f114 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/caps/EntityCapsTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/caps/EntityCapsTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2013-2018 Florian Schmaus + * Copyright 2013-2019 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -57,7 +57,7 @@ public class EntityCapsTest extends AbstractSmackIntegrationTest { private final ServiceDiscoveryManager sdmOne; private final ServiceDiscoveryManager sdmTwo; - public EntityCapsTest(SmackIntegrationTestEnvironment environment) { + public EntityCapsTest(SmackIntegrationTestEnvironment environment) { super(environment); ecmTwo = EntityCapsManager.getInstanceFor(environment.conTwo); sdmOne = ServiceDiscoveryManager.getInstanceFor(environment.conOne); diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/chatstate/ChatStateIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/chatstate/ChatStateIntegrationTest.java index 97133ff06..d2ffb6327 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/chatstate/ChatStateIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/chatstate/ChatStateIntegrationTest.java @@ -54,7 +54,7 @@ public class ChatStateIntegrationTest extends AbstractSmackIntegrationTest { }; - public ChatStateIntegrationTest(SmackIntegrationTestEnvironment environment) { + public ChatStateIntegrationTest(SmackIntegrationTestEnvironment environment) { super(environment); } diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/filetransfer/FileTransferIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/filetransfer/FileTransferIntegrationTest.java index a800eefea..b4d581647 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/filetransfer/FileTransferIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/filetransfer/FileTransferIntegrationTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015 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. @@ -39,7 +39,7 @@ public class FileTransferIntegrationTest extends AbstractSmackIntegrationTest { private final FileTransferManager ftManagerOne; private final FileTransferManager ftManagerTwo; - public FileTransferIntegrationTest(SmackIntegrationTestEnvironment environment) { + public FileTransferIntegrationTest(SmackIntegrationTestEnvironment environment) { super(environment); ftManagerOne = FileTransferManager.getInstanceFor(conOne); ftManagerTwo = FileTransferManager.getInstanceFor(conTwo); diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/httpfileupload/HttpFileUploadIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/httpfileupload/HttpFileUploadIntegrationTest.java index 4c75381bb..6390a2e94 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/httpfileupload/HttpFileUploadIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/httpfileupload/HttpFileUploadIntegrationTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2017 Florian Schmaus + * Copyright 2017-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. @@ -43,7 +43,7 @@ public class HttpFileUploadIntegrationTest extends AbstractSmackIntegrationTest private final HttpFileUploadManager hfumOne; - public HttpFileUploadIntegrationTest(SmackIntegrationTestEnvironment environment) throws XMPPErrorException, + public HttpFileUploadIntegrationTest(SmackIntegrationTestEnvironment environment) throws XMPPErrorException, NotConnectedException, NoResponseException, InterruptedException, TestNotPossibleException { super(environment); hfumOne = HttpFileUploadManager.getInstanceFor(conOne); diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/iot/IoTControlIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/iot/IoTControlIntegrationTest.java index 3d959f227..9d39fc30d 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/iot/IoTControlIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/iot/IoTControlIntegrationTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2016-2018 Florian Schmaus + * Copyright 2016-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. @@ -43,7 +43,7 @@ public class IoTControlIntegrationTest extends AbstractSmackIntegrationTest { private final IoTControlManager IoTControlManagerTwo; - public IoTControlIntegrationTest(SmackIntegrationTestEnvironment environment) { + public IoTControlIntegrationTest(SmackIntegrationTestEnvironment environment) { super(environment); IoTControlManagerOne = IoTControlManager.getInstanceFor(conOne); IoTControlManagerTwo = IoTControlManager.getInstanceFor(conTwo); diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/iot/IoTDataIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/iot/IoTDataIntegrationTest.java index 36a9831a3..90543108f 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/iot/IoTDataIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/iot/IoTDataIntegrationTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2016-2018 Florian Schmaus + * Copyright 2016-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. @@ -45,7 +45,7 @@ public class IoTDataIntegrationTest extends AbstractSmackIntegrationTest { private final IoTDataManager iotDataManagerTwo; - public IoTDataIntegrationTest(SmackIntegrationTestEnvironment environment) { + public IoTDataIntegrationTest(SmackIntegrationTestEnvironment environment) { super(environment); iotDataManagerOne = IoTDataManager.getInstanceFor(conOne); iotDataManagerTwo = IoTDataManager.getInstanceFor(conTwo); diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/iot/IoTDiscoveryIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/iot/IoTDiscoveryIntegrationTest.java index eac07899c..bf8457e6c 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/iot/IoTDiscoveryIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/iot/IoTDiscoveryIntegrationTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2016 Florian Schmaus + * Copyright 2016-2019 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,7 +41,7 @@ public class IoTDiscoveryIntegrationTest extends AbstractSmackIntegrationTest { private final IoTDiscoveryManager discoveryManagerOne; private final IoTDiscoveryManager discoveryManagerTwo; - public IoTDiscoveryIntegrationTest(SmackIntegrationTestEnvironment environment) throws NoResponseException, + public IoTDiscoveryIntegrationTest(SmackIntegrationTestEnvironment environment) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, TestNotPossibleException { super(environment); discoveryManagerOne = IoTDiscoveryManager.getInstanceFor(conOne); diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/iqversion/VersionIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/iqversion/VersionIntegrationTest.java index 173914a17..f22194452 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/iqversion/VersionIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/iqversion/VersionIntegrationTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015 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. @@ -31,7 +31,7 @@ import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; public class VersionIntegrationTest extends AbstractSmackIntegrationTest { - public VersionIntegrationTest(SmackIntegrationTestEnvironment environment) { + public VersionIntegrationTest(SmackIntegrationTestEnvironment environment) { super(environment); } diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/mam/MamIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/mam/MamIntegrationTest.java index 99168f78a..a7ac44581 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/mam/MamIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/mam/MamIntegrationTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2016 Fernando Ramirez, 2018 Florian Schmaus + * Copyright 2016 Fernando Ramirez, 2018-2019 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,7 +47,7 @@ public class MamIntegrationTest extends AbstractSmackIntegrationTest { private final MamManager mamManagerConTwo; - public MamIntegrationTest(SmackIntegrationTestEnvironment environment) throws NoResponseException, + public MamIntegrationTest(SmackIntegrationTestEnvironment environment) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, TestNotPossibleException, NotLoggedInException { super(environment); diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/mood/MoodIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/mood/MoodIntegrationTest.java index 2e1143aa7..e75693108 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/mood/MoodIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/mood/MoodIntegrationTest.java @@ -35,7 +35,7 @@ public class MoodIntegrationTest extends AbstractSmackIntegrationTest { private final MoodManager mm1; private final MoodManager mm2; - public MoodIntegrationTest(SmackIntegrationTestEnvironment environment) { + public MoodIntegrationTest(SmackIntegrationTestEnvironment environment) { super(environment); mm1 = MoodManager.getInstanceFor(conOne); mm2 = MoodManager.getInstanceFor(conTwo); 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 9fa199d08..5d96468c3 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 @@ -1,6 +1,6 @@ /** * - * Copyright 2015-2018 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. @@ -53,7 +53,7 @@ public class MultiUserChatIntegrationTest extends AbstractSmackIntegrationTest { private final MultiUserChatManager mucManagerTwo; private final DomainBareJid mucService; - public MultiUserChatIntegrationTest(SmackIntegrationTestEnvironment environment) + public MultiUserChatIntegrationTest(SmackIntegrationTestEnvironment environment) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, TestNotPossibleException { super(environment); diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatLowLevelIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatLowLevelIntegrationTest.java index 48d093d87..a47eef25f 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatLowLevelIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatLowLevelIntegrationTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015-2018 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. @@ -20,9 +20,9 @@ import static org.junit.Assert.assertTrue; import java.io.IOException; +import org.jivesoftware.smack.AbstractXMPPConnection; import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.XMPPException; -import org.jivesoftware.smack.tcp.XMPPTCPConnection; import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.smackx.bookmarks.BookmarkManager; @@ -40,20 +40,20 @@ import org.jxmpp.jid.parts.Resourcepart; public class MultiUserChatLowLevelIntegrationTest extends AbstractSmackLowLevelIntegrationTest { - public MultiUserChatLowLevelIntegrationTest(SmackIntegrationTestEnvironment environment) throws Exception { + public MultiUserChatLowLevelIntegrationTest(SmackIntegrationTestEnvironment environment) throws Exception { super(environment); - performCheck(new ConnectionCallback() { - @Override - public void connectionCallback(XMPPTCPConnection connection) throws Exception { - if (MultiUserChatManager.getInstanceFor(connection).getMucServiceDomains().isEmpty()) { - throw new TestNotPossibleException("MUC component not offered by service"); - } + AbstractXMPPConnection connection = getConnectedConnection(); + try { + if (MultiUserChatManager.getInstanceFor(connection).getMucServiceDomains().isEmpty()) { + throw new TestNotPossibleException("MUC component not offered by service"); } - }); + } finally { + recycle(connection); + } } @SmackIntegrationTest - public void testMucBookmarksAutojoin(XMPPTCPConnection connection) throws InterruptedException, + public void testMucBookmarksAutojoin(AbstractXMPPConnection connection) throws InterruptedException, TestNotPossibleException, XMPPException, SmackException, IOException { final BookmarkManager bookmarkManager = BookmarkManager.getBookmarkManager(connection); if (!bookmarkManager.isSupported()) { diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/AbstractOmemoIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/AbstractOmemoIntegrationTest.java index 225ffddb5..f16ca3d88 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/AbstractOmemoIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/AbstractOmemoIntegrationTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2017 Florian Schmaus, Paul Schaub + * Copyright 2017-2018 Florian Schmaus, Paul Schaub * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,7 @@ import org.igniterealtime.smack.inttest.TestNotPossibleException; */ public abstract class AbstractOmemoIntegrationTest extends AbstractSmackIntegrationTest { - public AbstractOmemoIntegrationTest(SmackIntegrationTestEnvironment environment) throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, TestNotPossibleException { + public AbstractOmemoIntegrationTest(SmackIntegrationTestEnvironment environment) throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, TestNotPossibleException { super(environment); // Test for server support diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/AbstractTwoUsersOmemoIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/AbstractTwoUsersOmemoIntegrationTest.java index 6d595ef39..4903fe72b 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/AbstractTwoUsersOmemoIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/AbstractTwoUsersOmemoIntegrationTest.java @@ -39,7 +39,7 @@ public abstract class AbstractTwoUsersOmemoIntegrationTest extends AbstractOmemo protected OmemoManager alice, bob; - public AbstractTwoUsersOmemoIntegrationTest(SmackIntegrationTestEnvironment environment) + public AbstractTwoUsersOmemoIntegrationTest(SmackIntegrationTestEnvironment environment) throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, TestNotPossibleException { super(environment); diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/MessageEncryptionIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/MessageEncryptionIntegrationTest.java index 63cccbf1b..33c06b560 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/MessageEncryptionIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/MessageEncryptionIntegrationTest.java @@ -34,7 +34,7 @@ import org.igniterealtime.smack.inttest.TestNotPossibleException; */ public class MessageEncryptionIntegrationTest extends AbstractTwoUsersOmemoIntegrationTest { - public MessageEncryptionIntegrationTest(SmackIntegrationTestEnvironment environment) + public MessageEncryptionIntegrationTest(SmackIntegrationTestEnvironment environment) throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, TestNotPossibleException { super(environment); diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoMamDecryptionTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoMamDecryptionTest.java index 1ff1e6d33..2c3017e74 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoMamDecryptionTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoMamDecryptionTest.java @@ -37,7 +37,7 @@ import org.igniterealtime.smack.inttest.TestNotPossibleException; * Then Bob fetches his Mam archive and decrypts the result. */ public class OmemoMamDecryptionTest extends AbstractTwoUsersOmemoIntegrationTest { - public OmemoMamDecryptionTest(SmackIntegrationTestEnvironment environment) + public OmemoMamDecryptionTest(SmackIntegrationTestEnvironment environment) throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, TestNotPossibleException { super(environment); diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/ReadOnlyDeviceIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/ReadOnlyDeviceIntegrationTest.java index 69d611ea4..3164d6668 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/ReadOnlyDeviceIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/ReadOnlyDeviceIntegrationTest.java @@ -32,7 +32,7 @@ import org.igniterealtime.smack.inttest.TestNotPossibleException; public class ReadOnlyDeviceIntegrationTest extends AbstractTwoUsersOmemoIntegrationTest { - public ReadOnlyDeviceIntegrationTest(SmackIntegrationTestEnvironment environment) throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, TestNotPossibleException { + public ReadOnlyDeviceIntegrationTest(SmackIntegrationTestEnvironment environment) throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, TestNotPossibleException { super(environment); } diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/SessionRenegotiationIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/SessionRenegotiationIntegrationTest.java index 90670b6be..28e4674c6 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/SessionRenegotiationIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/SessionRenegotiationIntegrationTest.java @@ -25,7 +25,7 @@ import org.igniterealtime.smack.inttest.TestNotPossibleException; public class SessionRenegotiationIntegrationTest extends AbstractTwoUsersOmemoIntegrationTest { - public SessionRenegotiationIntegrationTest(SmackIntegrationTestEnvironment environment) + public SessionRenegotiationIntegrationTest(SmackIntegrationTestEnvironment environment) throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, TestNotPossibleException { super(environment); diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/ox/AbstractOpenPgpIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/ox/AbstractOpenPgpIntegrationTest.java index a481d5274..2399c6999 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/ox/AbstractOpenPgpIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/ox/AbstractOpenPgpIntegrationTest.java @@ -41,7 +41,7 @@ public abstract class AbstractOpenPgpIntegrationTest extends AbstractSmackIntegr protected final PepManager bobPepManager; protected final PepManager chloePepManager; - protected AbstractOpenPgpIntegrationTest(SmackIntegrationTestEnvironment environment) + protected AbstractOpenPgpIntegrationTest(SmackIntegrationTestEnvironment environment) throws XMPPException.XMPPErrorException, TestNotPossibleException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException { super(environment); diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/ox/OXSecretKeyBackupIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/ox/OXSecretKeyBackupIntegrationTest.java index d3312f638..65c26d3ba 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/ox/OXSecretKeyBackupIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/ox/OXSecretKeyBackupIntegrationTest.java @@ -97,7 +97,7 @@ public class OXSecretKeyBackupIntegrationTest extends AbstractOpenPgpIntegration * @throws InterruptedException * @throws SmackException.NoResponseException */ - public OXSecretKeyBackupIntegrationTest(SmackIntegrationTestEnvironment environment) + public OXSecretKeyBackupIntegrationTest(SmackIntegrationTestEnvironment environment) throws XMPPException.XMPPErrorException, TestNotPossibleException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException { super(environment); diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/ox_im/OXInstantMessagingIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/ox_im/OXInstantMessagingIntegrationTest.java index a2be441ef..aa84f8122 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/ox_im/OXInstantMessagingIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/ox_im/OXInstantMessagingIntegrationTest.java @@ -87,7 +87,7 @@ public class OXInstantMessagingIntegrationTest extends AbstractOpenPgpIntegratio * @throws TestNotPossibleException if the test is not possible due to lacking server support for PEP. * @throws SmackException.NoResponseException */ - public OXInstantMessagingIntegrationTest(SmackIntegrationTestEnvironment environment) + public OXInstantMessagingIntegrationTest(SmackIntegrationTestEnvironment environment) throws XMPPException.XMPPErrorException, InterruptedException, SmackException.NotConnectedException, TestNotPossibleException, SmackException.NoResponseException { super(environment); diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/ping/PingIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/ping/PingIntegrationTest.java index 81b9b79d6..1d19c5b4f 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/ping/PingIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/ping/PingIntegrationTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015-2017 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. @@ -39,7 +39,7 @@ import org.jxmpp.jid.Jid; public class PingIntegrationTest extends AbstractSmackIntegrationTest { - public PingIntegrationTest(SmackIntegrationTestEnvironment environment) { + public PingIntegrationTest(SmackIntegrationTestEnvironment environment) { super(environment); } diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/pubsub/PubSubIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/pubsub/PubSubIntegrationTest.java index a8f19b6c5..12711d69f 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/pubsub/PubSubIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/pubsub/PubSubIntegrationTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015-2018 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 @@ public class PubSubIntegrationTest extends AbstractSmackIntegrationTest { private final PubSubManager pubSubManagerOne; - public PubSubIntegrationTest(SmackIntegrationTestEnvironment environment) + public PubSubIntegrationTest(SmackIntegrationTestEnvironment environment) throws TestNotPossibleException, NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { super(environment); diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/xdata/FormTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/xdata/FormTest.java index ffd8af0a4..40d31c161 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/xdata/FormTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/xdata/FormTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2004 Jive Software, 2017 Florian Schmaus. + * Copyright 2004 Jive Software, 2017-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. @@ -39,7 +39,7 @@ import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; */ public class FormTest extends AbstractSmackIntegrationTest { - public FormTest(SmackIntegrationTestEnvironment environment) { + public FormTest(SmackIntegrationTestEnvironment environment) { super(environment); } diff --git a/smack-integration-test/src/test/java/org/igniterealtime/smack/inttest/DummySmackIntegrationTestFramework.java b/smack-integration-test/src/test/java/org/igniterealtime/smack/inttest/DummySmackIntegrationTestFramework.java index c1140646a..6617f9b0a 100644 --- a/smack-integration-test/src/test/java/org/igniterealtime/smack/inttest/DummySmackIntegrationTestFramework.java +++ b/smack-integration-test/src/test/java/org/igniterealtime/smack/inttest/DummySmackIntegrationTestFramework.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015 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. @@ -16,21 +16,39 @@ */ package org.igniterealtime.smack.inttest; -import org.jivesoftware.smack.tcp.XMPPTCPConnection; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; -public class DummySmackIntegrationTestFramework extends SmackIntegrationTestFramework { +import org.jivesoftware.smack.DummyConnection; +import org.jivesoftware.smack.DummyConnection.DummyConnectionConfiguration; +import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.XMPPException; - public DummySmackIntegrationTestFramework(Configuration configuration) { - super(configuration); +public class DummySmackIntegrationTestFramework extends SmackIntegrationTestFramework { + + static { + try { + XmppConnectionManager.addConnectionDescriptor(DummyConnection.class, DummyConnectionConfiguration.class); + } catch (ClassNotFoundException | NoSuchMethodException | SecurityException e) { + throw new AssertionError(e); + } + } + + public DummySmackIntegrationTestFramework(Configuration configuration) throws KeyManagementException, + InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, + NoSuchAlgorithmException, SmackException, IOException, XMPPException, InterruptedException { + super(configuration, DummyConnection.class); + testRunResult = new TestRunResult(); } @Override - protected SmackIntegrationTestEnvironment prepareEnvironment() { - return new SmackIntegrationTestEnvironment(null, null, null, testRunResult.getTestRunId(), config); + protected SmackIntegrationTestEnvironment prepareEnvironment() { + DummyConnection dummyConnection = new DummyConnection(); + connectionManager.conOne = connectionManager.conTwo = connectionManager.conThree = dummyConnection; + return new SmackIntegrationTestEnvironment(dummyConnection, dummyConnection, dummyConnection, + testRunResult.getTestRunId(), config, null); } - @Override - protected void disconnectAndMaybeDelete(XMPPTCPConnection connection) { - // This method is a no-op in DummySmackIntegrationTestFramework - } } diff --git a/smack-integration-test/src/test/java/org/igniterealtime/smack/inttest/SmackIntegrationTestFrameWorkTest.java b/smack-integration-test/src/test/java/org/igniterealtime/smack/inttest/SmackIntegrationTestFrameWorkTest.java new file mode 100644 index 000000000..71ba75ac3 --- /dev/null +++ b/smack-integration-test/src/test/java/org/igniterealtime/smack/inttest/SmackIntegrationTestFrameWorkTest.java @@ -0,0 +1,96 @@ +/** + * + * 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. + * 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.igniterealtime.smack.inttest; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.lang.reflect.Method; +import java.util.List; + +import org.jivesoftware.smack.AbstractXMPPConnection; + +import org.junit.Test; + +public class SmackIntegrationTestFrameWorkTest { + + private static class ValidLowLevelList { + @SuppressWarnings("unused") + public void test(List connections) { + } + } + + private static class InvalidLowLevelList { + @SuppressWarnings("unused") + public void test(List connections, boolean invalid) { + } + } + + private static class ValidLowLevelVarargs { + @SuppressWarnings("unused") + public void test(AbstractXMPPConnection connectionOne, AbstractXMPPConnection connectionTwo, + AbstractXMPPConnection connectionThree) { + } + } + + private static class InvalidLowLevelVarargs { + @SuppressWarnings("unused") + public void test(AbstractXMPPConnection connectionOne, Integer invalid, AbstractXMPPConnection connectionTwo, + AbstractXMPPConnection connectionThree) { + } + } + + private static Method getTestMethod(Class testClass) { + Method[] methods = testClass.getDeclaredMethods(); + + for (Method method : methods) { + if (method.getName().equals("test")) { + return method; + } + } + + throw new IllegalArgumentException("No test method found in " + testClass); + } + + @Test + public void testValidLowLevelList() { + Method testMethod = getTestMethod(ValidLowLevelList.class); + assertTrue(SmackIntegrationTestFramework.testMethodParametersIsListOfConnections(testMethod, + AbstractXMPPConnection.class)); + } + + @Test + public void testInvalidLowLevelList() { + Method testMethod = getTestMethod(InvalidLowLevelList.class); + assertFalse(SmackIntegrationTestFramework.testMethodParametersIsListOfConnections(testMethod, + AbstractXMPPConnection.class)); + } + + @Test + public void testValidLowLevelVarargs() { + Method testMethod = getTestMethod(ValidLowLevelVarargs.class); + assertTrue(SmackIntegrationTestFramework.testMethodParametersVarargsConnections(testMethod, + AbstractXMPPConnection.class)); + } + + @Test + public void testInvalidLowLevelVargs() { + Method testMethod = getTestMethod(InvalidLowLevelVarargs.class); + assertFalse(SmackIntegrationTestFramework.testMethodParametersVarargsConnections(testMethod, + AbstractXMPPConnection.class)); + } +} diff --git a/smack-integration-test/src/test/java/org/igniterealtime/smack/inttest/SmackIntegrationTestUnitTestUtil.java b/smack-integration-test/src/test/java/org/igniterealtime/smack/inttest/SmackIntegrationTestUnitTestUtil.java index ba02dd4f6..de095b69a 100644 --- a/smack-integration-test/src/test/java/org/igniterealtime/smack/inttest/SmackIntegrationTestUnitTestUtil.java +++ b/smack-integration-test/src/test/java/org/igniterealtime/smack/inttest/SmackIntegrationTestUnitTestUtil.java @@ -16,9 +16,14 @@ */ package org.igniterealtime.smack.inttest; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; +import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.XMPPException; + import org.jxmpp.jid.JidTestUtil; public class SmackIntegrationTestUnitTestUtil { @@ -32,7 +37,12 @@ public class SmackIntegrationTestUnitTestUtil { .setUsernamesAndPassword("dummy1", "dummy1pass", "dummy2", "dummy2pass", "dummy3", "dummy3pass") .addEnabledTest(unitTest).build(); // @formatter:on - return new DummySmackIntegrationTestFramework(configuration); + try { + return new DummySmackIntegrationTestFramework(configuration); + } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException + | SmackException | IOException | XMPPException | InterruptedException e) { + throw new IllegalStateException(e); + } } } diff --git a/smack-integration-test/src/test/java/org/igniterealtime/smack/inttest/SmackIntegrationTestXmppConnectionManagerTest.java b/smack-integration-test/src/test/java/org/igniterealtime/smack/inttest/SmackIntegrationTestXmppConnectionManagerTest.java new file mode 100644 index 000000000..6cd7d0d1c --- /dev/null +++ b/smack-integration-test/src/test/java/org/igniterealtime/smack/inttest/SmackIntegrationTestXmppConnectionManagerTest.java @@ -0,0 +1,45 @@ +/** + * + * Copyright 2018-2019 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.igniterealtime.smack.inttest; + +import static org.junit.Assert.assertEquals; + +import java.lang.reflect.InvocationTargetException; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; + +import org.jivesoftware.smack.tcp.XMPPTCPConnectionConfiguration; +import org.jivesoftware.smack.tcp.XmppNioTcpConnection; + +import org.junit.Test; +import org.jxmpp.stringprep.XmppStringprepException; + +public class SmackIntegrationTestXmppConnectionManagerTest { + + @Test + public void simpleXmppConnectionDescriptorTest() throws ClassNotFoundException, NoSuchMethodException, + SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, + KeyManagementException, NoSuchAlgorithmException, XmppStringprepException, InstantiationException { + XmppConnectionDescriptor descriptor + = new XmppConnectionDescriptor<>(XmppNioTcpConnection.class, XMPPTCPConnectionConfiguration.class); + + Configuration sinttestConfiguration = Configuration.builder().setService("example.org").build(); + XmppNioTcpConnection connection = descriptor.construct(sinttestConfiguration); + + assertEquals("example.org", connection.getXMPPServiceDomain().toString()); + } +} diff --git a/smack-integration-test/src/test/java/org/igniterealtime/smack/inttest/unittest/SmackIntegrationTestFrameworkUnitTest.java b/smack-integration-test/src/test/java/org/igniterealtime/smack/inttest/unittest/SmackIntegrationTestFrameworkUnitTest.java index 9cd517049..d94a9b16c 100644 --- a/smack-integration-test/src/test/java/org/igniterealtime/smack/inttest/unittest/SmackIntegrationTestFrameworkUnitTest.java +++ b/smack-integration-test/src/test/java/org/igniterealtime/smack/inttest/unittest/SmackIntegrationTestFrameworkUnitTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015 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. @@ -22,6 +22,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import java.io.IOException; +import java.lang.reflect.InvocationTargetException; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.util.List; @@ -37,6 +38,7 @@ import org.igniterealtime.smack.inttest.DummySmackIntegrationTestFramework; import org.igniterealtime.smack.inttest.FailedTest; import org.igniterealtime.smack.inttest.SmackIntegrationTest; import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; +import org.igniterealtime.smack.inttest.SmackIntegrationTestFramework; import org.igniterealtime.smack.inttest.SmackIntegrationTestFramework.TestRunResult; import org.junit.AfterClass; import org.junit.BeforeClass; @@ -52,9 +54,19 @@ public class SmackIntegrationTestFrameworkUnitTest { private static boolean beforeClassInvoked; private static boolean afterClassInvoked; + @BeforeClass + public static void prepareSinttestUnitTest() { + SmackIntegrationTestFramework.SINTTEST_UNIT_TEST = true; + } + + @AfterClass + public static void disallowSinntestUnitTest() { + SmackIntegrationTestFramework.SINTTEST_UNIT_TEST = false; + } + @Test public void throwsRuntimeExceptionsTest() throws KeyManagementException, NoSuchAlgorithmException, SmackException, - IOException, XMPPException, InterruptedException { + IOException, XMPPException, InterruptedException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { expectedException.expect(RuntimeException.class); expectedException.expectMessage(ThrowsRuntimeExceptionDummyTest.RUNTIME_EXCEPTION_MESSAGE); DummySmackIntegrationTestFramework sinttest = getFrameworkForUnitTest(ThrowsRuntimeExceptionDummyTest.class); @@ -63,7 +75,7 @@ public class SmackIntegrationTestFrameworkUnitTest { public static class ThrowsRuntimeExceptionDummyTest extends AbstractSmackIntegrationTest { - public ThrowsRuntimeExceptionDummyTest(SmackIntegrationTestEnvironment environment) { + public ThrowsRuntimeExceptionDummyTest(SmackIntegrationTestEnvironment environment) { super(environment); } @@ -77,7 +89,8 @@ public class SmackIntegrationTestFrameworkUnitTest { @Test public void logsNonFatalExceptionTest() throws KeyManagementException, NoSuchAlgorithmException, SmackException, - IOException, XMPPException, InterruptedException { + IOException, XMPPException, InterruptedException, InstantiationException, IllegalAccessException, + IllegalArgumentException, InvocationTargetException { DummySmackIntegrationTestFramework sinttest = getFrameworkForUnitTest(ThrowsNonFatalExceptionDummyTest.class); TestRunResult testRunResult = sinttest.run(); List failedTests = testRunResult.getFailedTests(); @@ -93,7 +106,7 @@ public class SmackIntegrationTestFrameworkUnitTest { public static final String DESCRIPTIVE_TEXT = "I'm not fatal"; - public ThrowsNonFatalExceptionDummyTest(SmackIntegrationTestEnvironment environment) { + public ThrowsNonFatalExceptionDummyTest(SmackIntegrationTestEnvironment environment) { super(environment); } @@ -107,7 +120,8 @@ public class SmackIntegrationTestFrameworkUnitTest { @Test public void testInvoking() throws KeyManagementException, NoSuchAlgorithmException, SmackException, IOException, - XMPPException, InterruptedException { + XMPPException, InterruptedException, InstantiationException, IllegalAccessException, + IllegalArgumentException, InvocationTargetException { beforeClassInvoked = false; afterClassInvoked = false; @@ -120,7 +134,7 @@ public class SmackIntegrationTestFrameworkUnitTest { public static class BeforeAfterClassTest extends AbstractSmackIntegrationTest { - public BeforeAfterClassTest(SmackIntegrationTestEnvironment environment) { + public BeforeAfterClassTest(SmackIntegrationTestEnvironment environment) { super(environment); } diff --git a/smack-omemo-signal-integration-test/src/main/java/org/igniterealtime/smack/inttest/smack_omemo_signal/SmackOmemoSignalIntegrationTestFramework.java b/smack-omemo-signal-integration-test/src/main/java/org/igniterealtime/smack/inttest/smack_omemo_signal/SmackOmemoSignalIntegrationTestFramework.java index cdf1754f7..aa17e3593 100644 --- a/smack-omemo-signal-integration-test/src/main/java/org/igniterealtime/smack/inttest/smack_omemo_signal/SmackOmemoSignalIntegrationTestFramework.java +++ b/smack-omemo-signal-integration-test/src/main/java/org/igniterealtime/smack/inttest/smack_omemo_signal/SmackOmemoSignalIntegrationTestFramework.java @@ -1,6 +1,6 @@ /** * - * Copyright 2017 Paul Schaub + * Copyright 2017-2018 Paul Schaub * * This file is part of smack-omemo-signal-integration-test. * @@ -21,6 +21,7 @@ package org.igniterealtime.smack.inttest.smack_omemo_signal; import java.io.IOException; +import java.lang.reflect.InvocationTargetException; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.KeyManagementException; @@ -44,7 +45,8 @@ public class SmackOmemoSignalIntegrationTestFramework { public static void main(String[] args) throws InvalidKeyException, NoSuchPaddingException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException, NoSuchAlgorithmException, NoSuchProviderException, SmackException, - InterruptedException, CorruptedOmemoKeyException, KeyManagementException, IOException, XMPPException { + InterruptedException, CorruptedOmemoKeyException, KeyManagementException, IOException, XMPPException, + InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { SignalOmemoService.acknowledgeLicense(); SignalOmemoService.setup(); diff --git a/smack-repl/build.gradle b/smack-repl/build.gradle index d4fad4335..aac8253e9 100644 --- a/smack-repl/build.gradle +++ b/smack-repl/build.gradle @@ -37,3 +37,8 @@ task printClasspath(dependsOn: assemble) { println sourceSets.main.runtimeClasspath.asPath } } + +task printXmppNioTcpConnectionStateGraph(type: JavaExec) { + classpath sourceSets.main.runtimeClasspath + main 'org.igniterealtime.smack.smackrepl.StateGraph' +} diff --git a/smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/Nio.java b/smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/Nio.java new file mode 100644 index 000000000..8bb78013b --- /dev/null +++ b/smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/Nio.java @@ -0,0 +1,109 @@ +/** + * + * Copyright 2018 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.igniterealtime.smack.smackrepl; + +import java.io.IOException; +import java.util.Date; +import java.util.logging.Logger; + +import org.jivesoftware.smack.ConnectionConfiguration.SecurityMode; +import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.compression.XMPPInputOutputStream; +import org.jivesoftware.smack.compression.XMPPInputOutputStream.FlushMethod; +import org.jivesoftware.smack.debugger.ConsoleDebugger; +import org.jivesoftware.smack.debugger.SmackDebuggerFactory; +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.tcp.XMPPTCPConnectionConfiguration; +import org.jivesoftware.smack.tcp.XmppNioTcpConnection; + +import org.jxmpp.util.XmppDateTime; + +public class Nio { + + private static final Logger LOGGER = Logger.getLogger(Nio.class.getName()); + + public static void main(String[] args) throws SmackException, IOException, XMPPException, InterruptedException { + doNio(args[0], args[1], args[2]); + } + + public static void doNio(String username, String password, String service) + throws SmackException, IOException, XMPPException, InterruptedException { + boolean useTls = true; + boolean useCompression = true; + boolean useFullFlush = true; + boolean javaNetDebug = false; + boolean smackDebug = false; + + if (useFullFlush) { + XMPPInputOutputStream.setFlushMethod(FlushMethod.FULL_FLUSH); + } + + if (javaNetDebug) { + System.setProperty("javax.net.debug", "all"); + } + + final SecurityMode securityMode; + if (useTls) { + securityMode = SecurityMode.required; + } else { + securityMode = SecurityMode.disabled; + } + + final SmackDebuggerFactory smackDebuggerFactory; + if (smackDebug) { + smackDebuggerFactory = ConsoleDebugger.Factory.INSTANCE; + } else { + smackDebuggerFactory = null; + } + + XMPPTCPConnectionConfiguration configuration = XMPPTCPConnectionConfiguration.builder() + .setUsernameAndPassword(username, password) + .setXmppDomain(service) + .setDebuggerFactory(smackDebuggerFactory) + .setCompressionEnabled(useCompression) + .setSecurityMode(securityMode) + .build(); + + XmppNioTcpConnection connection = new XmppNioTcpConnection(configuration); + + connection.setReplyTimeout(5 * 60 * 1000); + + connection.addConnectionStateMachineListener((event, c) -> { + LOGGER.info("Connection event: " + event); + }); + + connection.connect(); + + connection.login(); + + Message message = new Message("flo@geekplace.eu", + "It is alive! " + XmppDateTime.formatXEP0082Date(new Date())); + connection.sendStanza(message); + + Thread.sleep(1000); + + connection.disconnect(); + + XmppNioTcpConnection.Stats connectionStats = connection.getStats(); + + // CHECKSTYLE:OFF + System.out.println("NIO successfully finished, yeah!\n" + connectionStats); + // CHECKSTYLE:ON + } + +} diff --git a/smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/StateGraph.java b/smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/StateGraph.java new file mode 100644 index 000000000..93596c8a0 --- /dev/null +++ b/smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/StateGraph.java @@ -0,0 +1,44 @@ +/** + * + * Copyright 2018 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.igniterealtime.smack.smackrepl; + +import java.io.PrintWriter; +import java.lang.reflect.InvocationTargetException; +import java.util.Collections; + +import org.jivesoftware.smack.fsm.StateDescriptor; +import org.jivesoftware.smack.fsm.StateDescriptorGraph; +import org.jivesoftware.smack.fsm.StateDescriptorGraph.GraphVertex; +import org.jivesoftware.smack.tcp.XmppNioTcpConnection; + +public class StateGraph { + + @SuppressWarnings("DefaultCharset") + public static void main(String[] args) throws InstantiationException, IllegalAccessException, + IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException { + GraphVertex stateGraph = StateDescriptorGraph.constructStateDescriptorGraph(XmppNioTcpConnection.getBackwardEdgesStateDescriptors()); + + PrintWriter pw = new PrintWriter(System.out); + + boolean breakStateName = args.length == 0; + + StateDescriptorGraph.stateDescriptorGraphToDot(Collections.singleton(stateGraph), pw, breakStateName); + + pw.flush(); + } + +} diff --git a/smack-resolver-minidns/src/main/java/org/jivesoftware/smack/util/dns/minidns/MiniDnsDaneVerifier.java b/smack-resolver-minidns/src/main/java/org/jivesoftware/smack/util/dns/minidns/MiniDnsDaneVerifier.java index d73a332c8..b00599211 100644 --- a/smack-resolver-minidns/src/main/java/org/jivesoftware/smack/util/dns/minidns/MiniDnsDaneVerifier.java +++ b/smack-resolver-minidns/src/main/java/org/jivesoftware/smack/util/dns/minidns/MiniDnsDaneVerifier.java @@ -23,6 +23,7 @@ import java.util.logging.Logger; import javax.net.ssl.KeyManager; import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocket; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; @@ -68,4 +69,17 @@ public class MiniDnsDaneVerifier implements SmackDaneVerifier { } } + @Override + public void finish(SSLSession sslSession) throws CertificateException { + if (VERIFIER.verify(sslSession)) { + // DANE verification was the only requirement according to the TLSA RR. We can return here. + return; + } + + // DANE verification was successful, but according to the TLSA RR we also must perform PKIX validation. + if (expectingTrustManager.hasException()) { + // PKIX validation has failed. Throw an exception. + throw expectingTrustManager.getException(); + } + } } diff --git a/smack-tcp/Makefile b/smack-tcp/Makefile new file mode 100644 index 000000000..00e349208 --- /dev/null +++ b/smack-tcp/Makefile @@ -0,0 +1,12 @@ +.PHONY := clean generate + +GRADLE_QUITE_ARGS := --quiet --console plain + +GENERATED_FILES := src/javadoc/org/jivesoftware/smack/tcp/doc-files/XmppNioTcpConnectionStateGraph.png +generate: $(GENERATED_FILES) + +clean: + rm -f $(GENERATED_FILES) + +src/javadoc/org/jivesoftware/smack/tcp/doc-files/XmppNioTcpConnectionStateGraph.png: src/main/java/org/jivesoftware/smack/tcp/XmppNioTcpConnection.java ../smack-core/src/main/java/org/jivesoftware/smack/fsm/AbstractXmppStateMachineConnection.java + gradle $(GRADLE_QUITE_ARGS) :smack-repl:printXmppNioTcpConnectionStateGraph | dot -Tpng -o $@ diff --git a/smack-tcp/src/javadoc/org/jivesoftware/smack/tcp/doc-files/.gitignore b/smack-tcp/src/javadoc/org/jivesoftware/smack/tcp/doc-files/.gitignore new file mode 100644 index 000000000..e68066393 --- /dev/null +++ b/smack-tcp/src/javadoc/org/jivesoftware/smack/tcp/doc-files/.gitignore @@ -0,0 +1 @@ +/XmppNioTcpConnectionStateGraph.png 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 a58172c41..a6d1bf78a 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 @@ -17,26 +17,19 @@ package org.jivesoftware.smack.tcp; import java.io.BufferedReader; -import java.io.ByteArrayInputStream; -import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Writer; -import java.lang.reflect.Constructor; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Socket; import java.security.KeyManagementException; -import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; -import java.security.Provider; -import java.security.SecureRandom; -import java.security.Security; import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; import java.util.ArrayList; @@ -58,21 +51,12 @@ import java.util.logging.Logger; import javax.net.SocketFactory; import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.KeyManager; -import javax.net.ssl.KeyManagerFactory; -import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocket; -import javax.net.ssl.TrustManager; -import javax.net.ssl.X509TrustManager; -import javax.security.auth.callback.Callback; -import javax.security.auth.callback.CallbackHandler; -import javax.security.auth.callback.PasswordCallback; import org.jivesoftware.smack.AbstractConnectionListener; import org.jivesoftware.smack.AbstractXMPPConnection; import org.jivesoftware.smack.ConnectionConfiguration; -import org.jivesoftware.smack.ConnectionConfiguration.DnssecMode; import org.jivesoftware.smack.ConnectionConfiguration.SecurityMode; import org.jivesoftware.smack.SmackConfiguration; import org.jivesoftware.smack.SmackException; @@ -127,14 +111,11 @@ import org.jivesoftware.smack.sm.provider.ParseStreamManagement; import org.jivesoftware.smack.util.ArrayBlockingQueueWithShutdown; import org.jivesoftware.smack.util.Async; import org.jivesoftware.smack.util.CloseableUtil; -import org.jivesoftware.smack.util.DNSUtil; import org.jivesoftware.smack.util.PacketParserUtils; import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.smack.util.TLSUtils; import org.jivesoftware.smack.util.XmlStringBuilder; import org.jivesoftware.smack.util.dns.HostAddress; -import org.jivesoftware.smack.util.dns.SmackDaneProvider; -import org.jivesoftware.smack.util.dns.SmackDaneVerifier; import org.jxmpp.jid.impl.JidCreate; import org.jxmpp.jid.parts.Resourcepart; @@ -192,13 +173,6 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { private final SynchronizationPoint compressSyncPoint = new SynchronizationPoint<>( this, "stream compression"); - /** - * A synchronization point which is successful if this connection has received the closing - * stream element from the remote end-point, i.e. the server. - */ - private final SynchronizationPoint closingStreamReceived = new SynchronizationPoint<>( - this, "stream closing element received"); - /** * The default bundle and defer callback, used for new connections. * @see bundleAndDeferCallback @@ -478,6 +452,7 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { /** * Performs an unclean disconnect and shutdown of the connection. Does not send a closing stream stanza. */ + @Override public synchronized void instantShutdown() { shutdown(true); } @@ -496,15 +471,7 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { LOGGER.finer("PacketWriter has been shut down"); if (!instant) { - try { - // 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. - @SuppressWarnings("unused") - Exception res = closingStreamReceived.checkIfSuccessOrWait(); - } catch (InterruptedException | NoResponseException e) { - LOGGER.log(Level.INFO, "Exception while waiting for closing stream element from the server " + this, e); - } + waitForClosingStreamTagFromServer(); } if (packetReader != null) { @@ -682,117 +649,11 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { */ @SuppressWarnings("LiteralClassName") private void proceedTLSReceived() throws NoSuchAlgorithmException, CertificateException, IOException, KeyStoreException, NoSuchProviderException, UnrecoverableKeyException, KeyManagementException, SmackException { - SmackDaneVerifier daneVerifier = null; - - if (config.getDnssecMode() == DnssecMode.needsDnssecAndDane) { - SmackDaneProvider daneProvider = DNSUtil.getDaneProvider(); - if (daneProvider == null) { - throw new UnsupportedOperationException("DANE enabled but no SmackDaneProvider configured"); - } - daneVerifier = daneProvider.newInstance(); - if (daneVerifier == null) { - throw new IllegalStateException("DANE requested but DANE provider did not return a DANE verifier"); - } - } - - SSLContext context = this.config.getCustomSSLContext(); - KeyStore ks = null; - PasswordCallback pcb = null; - - if (context == null) { - final String keyStoreType = config.getKeystoreType(); - final CallbackHandler callbackHandler = config.getCallbackHandler(); - final String keystorePath = config.getKeystorePath(); - if ("PKCS11".equals(keyStoreType)) { - try { - Constructor c = Class.forName("sun.security.pkcs11.SunPKCS11").getConstructor(InputStream.class); - String pkcs11Config = "name = SmartCard\nlibrary = " + config.getPKCS11Library(); - ByteArrayInputStream config = new ByteArrayInputStream(pkcs11Config.getBytes(StringUtils.UTF8)); - Provider p = (Provider) c.newInstance(config); - Security.addProvider(p); - ks = KeyStore.getInstance("PKCS11",p); - pcb = new PasswordCallback("PKCS11 Password: ",false); - callbackHandler.handle(new Callback[] {pcb}); - ks.load(null,pcb.getPassword()); - } - catch (Exception e) { - LOGGER.log(Level.WARNING, "Exception", e); - ks = null; - } - } - else if ("Apple".equals(keyStoreType)) { - ks = KeyStore.getInstance("KeychainStore","Apple"); - ks.load(null,null); - // pcb = new PasswordCallback("Apple Keychain",false); - // pcb.setPassword(null); - } - else if (keyStoreType != null) { - ks = KeyStore.getInstance(keyStoreType); - if (callbackHandler != null && StringUtils.isNotEmpty(keystorePath)) { - try { - pcb = new PasswordCallback("Keystore Password: ", false); - callbackHandler.handle(new Callback[] { pcb }); - ks.load(new FileInputStream(keystorePath), pcb.getPassword()); - } - catch (Exception e) { - LOGGER.log(Level.WARNING, "Exception", e); - ks = null; - } - } else { - ks.load(null, null); - } - } - - KeyManager[] kms = null; - - if (ks != null) { - String keyManagerFactoryAlgorithm = KeyManagerFactory.getDefaultAlgorithm(); - KeyManagerFactory kmf = null; - try { - kmf = KeyManagerFactory.getInstance(keyManagerFactoryAlgorithm); - } - catch (NoSuchAlgorithmException e) { - LOGGER.log(Level.FINE, "Could get the default KeyManagerFactory for the '" - + keyManagerFactoryAlgorithm + "' algorithm", e); - } - if (kmf != null) { - try { - if (pcb == null) { - kmf.init(ks, null); - } - else { - kmf.init(ks, pcb.getPassword()); - pcb.clearPassword(); - } - kms = kmf.getKeyManagers(); - } - catch (NullPointerException npe) { - LOGGER.log(Level.WARNING, "NullPointerException", npe); - } - } - } - - // If the user didn't specify a SSLContext, use the default one - context = SSLContext.getInstance("TLS"); - - final SecureRandom secureRandom = new java.security.SecureRandom(); - X509TrustManager customTrustManager = config.getCustomX509TrustManager(); - - if (daneVerifier != null) { - // User requested DANE verification. - daneVerifier.init(context, kms, customTrustManager, secureRandom); - } else { - TrustManager[] customTrustManagers = null; - if (customTrustManager != null) { - customTrustManagers = new TrustManager[] { customTrustManager }; - } - context.init(kms, customTrustManagers, secureRandom); - } - } + SmackTlsContext smackTlsContext = getSmackTlsContext(); Socket plain = socket; // Secure the plain connection - socket = context.getSocketFactory().createSocket(plain, + socket = smackTlsContext.sslContext.getSocketFactory().createSocket(plain, config.getXMPPServiceDomain().toString(), plain.getPort(), true); final SSLSocket sslSocket = (SSLSocket) socket; @@ -807,8 +668,8 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { // Proceed to do the handshake sslSocket.startHandshake(); - if (daneVerifier != null) { - daneVerifier.finish(sslSocket); + if (smackTlsContext.daneVerifier != null) { + smackTlsContext.daneVerifier.finish(sslSocket); } final HostnameVerifier verifier = getConfiguration().getHostnameVerifier(); @@ -909,26 +770,6 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { saslFeatureReceived.checkIfSuccessOrWaitOrThrow(); } - /** - * Sends out a notification that there was an error with the connection - * and closes the connection. Also prints the stack trace of the given exception - * - * @param e the exception that causes the connection close event. - */ - private synchronized void notifyConnectionError(Exception e) { - // Listeners were already notified of the exception, return right here. - if ((packetReader == null || packetReader.done) && - (packetWriter == null || packetWriter.done())) return; - - // Closes the connection temporary. A reconnection is possible - // Note that a connection listener of XMPPTCPConnection will drop the SM state in - // case the Exception is a StreamErrorException. - instantShutdown(); - - // Notify connection listeners of the error. - callConnectionClosedOnErrorListener(e); - } - /** * For unit testing purposes * @@ -975,18 +816,7 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { * @throws InterruptedException */ void openStream() throws SmackException, InterruptedException { - // If possible, provide the receiving entity of the stream open tag, i.e. the server, as much information as - // possible. The 'to' attribute is *always* available. The 'from' attribute if set by the user and no external - // mechanism is used to determine the local entity (user). And the 'id' attribute is available after the first - // response from the server (see e.g. RFC 6120 ยง 9.1.1 Step 2.) - CharSequence to = getXMPPServiceDomain(); - CharSequence from = null; - CharSequence localpart = config.getUsername(); - if (localpart != null) { - from = XmppStringUtils.completeJidFrom(localpart, to); - } - String id = getStreamId(); - sendNonza(new StreamOpen(to, from, id)); + sendStreamOpen(); try { packetReader.parser = PacketParserUtils.newXmppParser(reader); } @@ -1045,12 +875,7 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { } break; case "stream": - // We found an opening stream. - if ("jabber:client".equals(parser.getNamespace(null))) { - streamId = parser.getAttributeValue("", "id"); - String reportedServerDomain = parser.getAttributeValue("", "from"); - assert (config.getXMPPServiceDomain().equals(reportedServerDomain)); - } + onStreamOpen(parser); break; case "error": StreamError streamError = PacketParserUtils.parseStreamError(parser); @@ -1061,7 +886,7 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { tlsHandled.reportSuccess(); throw new StreamErrorException(streamError); case "features": - parseFeatures(parser); + parseFeaturesAndNotify(parser); break; case "proceed": try { diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XmppNioTcpConnection.java b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XmppNioTcpConnection.java new file mode 100644 index 000000000..efc2f322e --- /dev/null +++ b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XmppNioTcpConnection.java @@ -0,0 +1,1865 @@ +/** + * + * Copyright 2018-2019 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.tcp; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.nio.Buffer; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.SelectionKey; +import java.nio.channels.SocketChannel; +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; +import java.util.Collections; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.ReentrantLock; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLEngineResult; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLSession; + +import org.jivesoftware.smack.AbstractXmppNioConnection; +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.SmackReactor.ChannelSelectedCallback; +import org.jivesoftware.smack.SmackReactor.SelectionKeyAttachment; +import org.jivesoftware.smack.SynchronizationPoint; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.XMPPException.FailedNonzaException; +import org.jivesoftware.smack.XMPPException.XMPPErrorException; +import org.jivesoftware.smack.XmppInputOutputFilter; +import org.jivesoftware.smack.fsm.ConnectionStateEvent.DetailedTransitionIntoInformation; +import org.jivesoftware.smack.fsm.StateDescriptor; +import org.jivesoftware.smack.fsm.StateDescriptorGraph; +import org.jivesoftware.smack.fsm.StateDescriptorGraph.GraphVertex; +import org.jivesoftware.smack.packet.Nonza; +import org.jivesoftware.smack.packet.Stanza; +import org.jivesoftware.smack.packet.StartTls; +import org.jivesoftware.smack.packet.StreamClose; +import org.jivesoftware.smack.packet.StreamOpen; +import org.jivesoftware.smack.packet.TlsFailure; +import org.jivesoftware.smack.packet.TlsProceed; +import org.jivesoftware.smack.packet.TopLevelStreamElement; +import org.jivesoftware.smack.sasl.SASLErrorException; +import org.jivesoftware.smack.util.ArrayBlockingQueueWithShutdown; +import org.jivesoftware.smack.util.Async; +import org.jivesoftware.smack.util.CollectionUtil; +import org.jivesoftware.smack.util.StringUtils; +import org.jivesoftware.smack.util.UTF8; +import org.jivesoftware.smack.util.XmlStringBuilder; +import org.jivesoftware.smack.util.dns.HostAddress; + +import org.jxmpp.jid.DomainBareJid; +import org.jxmpp.jid.Jid; +import org.jxmpp.jid.impl.JidCreate; +import org.jxmpp.jid.util.JidUtil; +import org.jxmpp.stringprep.XmppStringprepException; +import org.jxmpp.xml.splitter.Utf8ByteXmppXmlSplitter; +import org.jxmpp.xml.splitter.XmlPrettyPrinter; +import org.jxmpp.xml.splitter.XmlPrinter; +import org.jxmpp.xml.splitter.XmppElementCallback; +import org.jxmpp.xml.splitter.XmppXmlSplitter; + +/** + * Represents and manages a client connection to an XMPP server via TCP. + * + *

Smack XMPP TCP NIO connection states

+ *

+ * The graph below shows the current graph of states of this XMPP connection. Only some states are final states, most + * states are intermediate states in order to reach a final state. + *

+ * The state graph of XmppNioTcpConnection + * + */ +public class XmppNioTcpConnection extends AbstractXmppNioConnection { + + private static final Logger LOGGER = Logger.getLogger(XmppNioTcpConnection.class.getName()); + + private static final Set> BACKWARD_EDGES_STATE_DESCRIPTORS = new HashSet<>(); + + static final GraphVertex INITIAL_STATE_DESCRIPTOR_VERTEX; + + static { + BACKWARD_EDGES_STATE_DESCRIPTORS.add(LookupHostAddressesStateDescriptor.class); + BACKWARD_EDGES_STATE_DESCRIPTORS.add(EnableStreamManagementStateDescriptor.class); + BACKWARD_EDGES_STATE_DESCRIPTORS.add(ResumeStreamStateDescriptor.class); + BACKWARD_EDGES_STATE_DESCRIPTORS.add(InstantStreamResumptionStateDescriptor.class); + BACKWARD_EDGES_STATE_DESCRIPTORS.add(Bind2StateDescriptor.class); + BACKWARD_EDGES_STATE_DESCRIPTORS.add(InstantShutdownStateDescriptor.class); + BACKWARD_EDGES_STATE_DESCRIPTORS.add(ShutdownStateDescriptor.class); + + try { + INITIAL_STATE_DESCRIPTOR_VERTEX = StateDescriptorGraph.constructStateDescriptorGraph(BACKWARD_EDGES_STATE_DESCRIPTORS); + } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException + | NoSuchMethodException | SecurityException e) { + throw new IllegalStateException(e); + } + } + + private static final int CALLBACK_MAX_BYTES_READ = 10 * 1024 * 1024; + private static final int CALLBACK_MAX_BYTES_WRITEN = CALLBACK_MAX_BYTES_READ; + + private static final int MAX_ELEMENT_SIZE = 64 * 1024; + + private SelectionKey selectionKey; + private SelectionKeyAttachment selectionKeyAttachment; + private SocketChannel socketChannel; + private InetSocketAddress remoteAddress; + private TlsState tlsState; + + /** + * Note that this field is effective final, but due to https://stackoverflow.com/q/30360824/194894 we have to declare it non-final. + */ + private Utf8ByteXmppXmlSplitter splitter; + + /** + * Note that this field is effective final, but due to https://stackoverflow.com/q/30360824/194894 we have to declare it non-final. + */ + private XmppXmlSplitter outputDebugSplitter; + + private static final Level STREAM_OPEN_CLOSE_DEBUG_LOG_LEVEL = Level.FINER; + + private final XmppElementCallback xmppElementCallback = new XmppElementCallback() { + private String streamOpen; + private String streamClose; + + @Override + public void onCompleteElement(String completeElement) { + assert streamOpen != null; + assert streamClose != null; + + if (debugger != null) { + debugger.onIncomingElementCompleted(); + } + + String wrappedCompleteElement = streamOpen + completeElement + streamClose; + try { + parseAndProcessElement(wrappedCompleteElement); + } catch (Exception e) { + notifyConnectionError(e); + } + } + + + @Override + public void streamOpened(String prefix, Map attributes) { + if (LOGGER.isLoggable(STREAM_OPEN_CLOSE_DEBUG_LOG_LEVEL)) { + LOGGER.log(STREAM_OPEN_CLOSE_DEBUG_LOG_LEVEL, + "Stream of " + this + " opened. prefix=" + prefix + " attributes=" + attributes); + } + + final String prefixXmlns = "xmlns:" + prefix; + final StringBuilder streamClose = new StringBuilder(32); + final StringBuilder streamOpen = new StringBuilder(256); + + streamOpen.append('<'); + streamClose.append(""); + for (Entry entry : attributes.entrySet()) { + String attributeName = entry.getKey(); + String attributeValue = entry.getValue(); + switch (attributeName) { + case "id": + streamId = attributeValue; + break; + case "version": + break; + case "xml:lang": + streamOpen.append(" xml:lang='").append(attributeValue).append('\''); + break; + case "to": + break; + case "from": + DomainBareJid reportedServerDomain; + try { + reportedServerDomain = JidCreate.domainBareFrom(attributeValue); + } catch (XmppStringprepException e) { + IllegalStateException ise = new IllegalStateException( + "Reporting server domain '" + attributeValue + "' is not a valid JID", e); + notifyConnectionError(ise); + return; + } + assert (config.getXMPPServiceDomain().equals(reportedServerDomain)); + break; + case "xmlns": + streamOpen.append(" xmlns='").append(attributeValue).append('\''); + break; + default: + if (attributeName.equals(prefixXmlns)) { + streamOpen.append(' ').append(prefixXmlns).append("='").append(attributeValue).append('\''); + break; + } + LOGGER.info("Unknown attribute: " + attributeName); + break; + } + } + streamOpen.append('>'); + + this.streamOpen = streamOpen.toString(); + this.streamClose = streamClose.toString(); + } + + @Override + public void streamClosed() { + if (LOGGER.isLoggable(STREAM_OPEN_CLOSE_DEBUG_LOG_LEVEL)) { + LOGGER.log(STREAM_OPEN_CLOSE_DEBUG_LOG_LEVEL, "Stream of " + this + " closed"); + } + + closingStreamReceived.reportSuccess(); + } + }; + + private final ArrayBlockingQueueWithShutdown outgoingElementsQueue = new ArrayBlockingQueueWithShutdown<>( + 100, true); + + private Iterator outgoingCharSequenceIterator; + + private final List currentlyOutgoingElements = new ArrayList<>(); + private final Map> bufferToElementMap = new IdentityHashMap<>(); + + private ByteBuffer outgoingBuffer; + private ByteBuffer filteredOutgoingBuffer; + private final List networkOutgoingBuffers = new ArrayList<>(); + private long networkOutgoingBuffersBytes; + + // TODO: Make the size of the incomingBuffer configurable. + private final ByteBuffer incomingBuffer = ByteBuffer.allocateDirect(2 * 4096); + + private final ReentrantLock channelSelectedCallbackLock = new ReentrantLock(); + + private long totalBytesRead; + private long totalBytesWritten; + private long totalBytesReadAfterFilter; + private long totalBytesWrittenBeforeFilter; + private long handledChannelSelectedCallbacks; + private long callbackPreemtBecauseBytesWritten; + private long callbackPreemtBecauseBytesRead; + private int sslEngineDelegatedTasks; + private int maxPendingSslEngineDelegatedTasks; + + // TODO: Use LongAdder once Smack's minimum Android API level is 24 or higher. + private final AtomicLong setWriteInterestAfterChannelSelectedCallback = new AtomicLong(); + private final AtomicLong reactorThreadAlreadyRacing = new AtomicLong(); + private final AtomicLong afterOutgoingElementsQueueModifiedSetInterestOps = new AtomicLong(); + private final AtomicLong rejectedChannelSelectedCallbacks = new AtomicLong(); + + private Jid lastDestinationAddress; + + private boolean pendingInputFilterData; + private boolean pendingOutputFilterData; + + private boolean pendingWriteInterestAfterRead; + + private boolean useDirectTls = false; + + private boolean useSm = false; + private boolean useSmResumption = false; + private boolean useIsr = false; + + private boolean useBind2 = false; + + public XmppNioTcpConnection(XMPPTCPConnectionConfiguration configuration) { + super(configuration, INITIAL_STATE_DESCRIPTOR_VERTEX); + + XmlPrinter incomingDebugPrettyPrinter = null; + if (debugger != null) { + // Incoming stream debugging. + incomingDebugPrettyPrinter = XmlPrettyPrinter.builder() + .setPrettyWriter(sb -> debugger.incomingStreamSink(sb)) + .build(); + + // Outgoing stream debugging. + XmlPrinter outgoingDebugPrettyPrinter = XmlPrettyPrinter.builder() + .setPrettyWriter(sb -> debugger.outgoingStreamSink(sb)) + .build(); + outputDebugSplitter = new XmppXmlSplitter(outgoingDebugPrettyPrinter); + } + + XmppXmlSplitter xmppXmlSplitter = new XmppXmlSplitter(MAX_ELEMENT_SIZE, xmppElementCallback, + incomingDebugPrettyPrinter); + splitter = new Utf8ByteXmppXmlSplitter(xmppXmlSplitter); + } + + private final ChannelSelectedCallback channelSelectedCallback = + (selectedChannel, selectedSelectionKey) -> { + assert selectionKey == null || selectionKey == selectedSelectionKey; + SocketChannel selectedSocketChannel = (SocketChannel) selectedChannel; + // We are *always* interested in OP_READ. + int newInterestedOps = SelectionKey.OP_READ; + boolean newPendingOutputFilterData = false; + + if (!channelSelectedCallbackLock.tryLock()) { + rejectedChannelSelectedCallbacks.incrementAndGet(); + return; + } + + // LOGGER.info("Accepted channel selected callback"); + + handledChannelSelectedCallbacks++; + + long callbackBytesRead = 0; + long callbackBytesWritten = 0; + + try { + boolean destinationAddressChanged = false; + boolean isLastPartOfElement = false; + TopLevelStreamElement currentlyOutgonigTopLevelStreamElement = null; + StringBuilder outgoingStreamForDebugger = null; + + writeLoop: while (true) { + final boolean moreDataAvailable = !isLastPartOfElement || !outgoingElementsQueue.isEmpty(); + + if (filteredOutgoingBuffer != null || !networkOutgoingBuffers.isEmpty()) { + if (filteredOutgoingBuffer != null) { + networkOutgoingBuffers.add(filteredOutgoingBuffer); + networkOutgoingBuffersBytes += filteredOutgoingBuffer.remaining(); + + filteredOutgoingBuffer = null; + if (moreDataAvailable && networkOutgoingBuffersBytes < 8096) { + continue; + } + } + + ByteBuffer[] output = networkOutgoingBuffers.toArray(new ByteBuffer[networkOutgoingBuffers.size()]); + long bytesWritten; + try { + bytesWritten = selectedSocketChannel.write(output); + } catch (IOException e) { + // We have seen here so far + // - IOException "Broken pipe" + handleReadWriteIoException(e); + break; + } + + if (bytesWritten == 0) { + newInterestedOps |= SelectionKey.OP_WRITE; + break; + } + + callbackBytesWritten += bytesWritten; + + networkOutgoingBuffersBytes -= bytesWritten; + + List prunedBuffers = pruneBufferList(networkOutgoingBuffers); + + for (Buffer prunedBuffer : prunedBuffers) { + List sendElements = bufferToElementMap.remove(prunedBuffer); + if (sendElements == null) { + continue; + } + for (TopLevelStreamElement elementJustSend : sendElements) { + firePacketSendingListeners(elementJustSend); + } + } + + // Prevent one callback from dominating the reactor thread. Break out of the write-loop if we have + // written a certain amount. + if (callbackBytesWritten > CALLBACK_MAX_BYTES_WRITEN) { + newInterestedOps |= SelectionKey.OP_WRITE; + callbackPreemtBecauseBytesWritten++; + break; + } + } else if (outgoingBuffer != null || pendingOutputFilterData) { + pendingOutputFilterData = false; + + if (outgoingBuffer != null) { + totalBytesWrittenBeforeFilter += outgoingBuffer.remaining(); + if (isLastPartOfElement) { + assert currentlyOutgonigTopLevelStreamElement != null; + currentlyOutgoingElements.add(currentlyOutgonigTopLevelStreamElement); + } + } + + ByteBuffer outputFilterInputData = outgoingBuffer; + // We can now null the outgoingBuffer since the filter step will take care of it from now on. + outgoingBuffer = null; + + for (ListIterator it = getXmppInputOutputFilterBeginIterator(); it.hasNext();) { + XmppInputOutputFilter inputOutputFilter = it.next(); + XmppInputOutputFilter.OutputResult outputResult; + try { + outputResult = inputOutputFilter.output(outputFilterInputData, isLastPartOfElement, + destinationAddressChanged, moreDataAvailable); + } catch (IOException e) { + notifyConnectionError(e); + break writeLoop; + } + newPendingOutputFilterData |= outputResult.pendingFilterData; + outputFilterInputData = outputResult.filteredOutputData; + if (outputFilterInputData != null) { + outputFilterInputData.flip(); + } + } + + // It is ok if outpuFilterInputData is 'null' here, this is expected behavior. + if (outputFilterInputData != null && outputFilterInputData.hasRemaining()) { + filteredOutgoingBuffer = outputFilterInputData; + } else { + filteredOutgoingBuffer = null; + } + + // If the filters did eventually not produce any output data but if there is + // pending output data then we have a pending write request after read. + if (filteredOutgoingBuffer == null && newPendingOutputFilterData) { + pendingWriteInterestAfterRead = true; + } + + if (filteredOutgoingBuffer != null && isLastPartOfElement) { + bufferToElementMap.put(filteredOutgoingBuffer, new ArrayList<>(currentlyOutgoingElements)); + currentlyOutgoingElements.clear(); + } + + // Reset that the destination address has changed. + if (destinationAddressChanged) { + destinationAddressChanged = false; + } + } else if (outgoingCharSequenceIterator != null) { + CharSequence nextCharSequence = outgoingCharSequenceIterator.next(); + outgoingBuffer = UTF8.encode(nextCharSequence); + if (!outgoingCharSequenceIterator.hasNext()) { + outgoingCharSequenceIterator = null; + isLastPartOfElement = true; + } else { + isLastPartOfElement = false; + } + + if (debugger != null) { + if (outgoingStreamForDebugger == null) { + outgoingStreamForDebugger = new StringBuilder(); + } + outgoingStreamForDebugger.append(nextCharSequence); + + if (isLastPartOfElement) { + try { + outputDebugSplitter.append(outgoingStreamForDebugger); + } catch (IOException e) { + throw new AssertionError(e); + } + debugger.onOutgoingElementCompleted(); + outgoingStreamForDebugger = null; + } + } + } else if (!outgoingElementsQueue.isEmpty()) { + currentlyOutgonigTopLevelStreamElement = outgoingElementsQueue.poll(); + if (currentlyOutgonigTopLevelStreamElement instanceof Stanza) { + Stanza currentlyOutgoingStanza = (Stanza) currentlyOutgonigTopLevelStreamElement; + Jid currentDestinationAddress = currentlyOutgoingStanza.getTo(); + destinationAddressChanged = !JidUtil.equals(lastDestinationAddress, currentDestinationAddress); + lastDestinationAddress = currentDestinationAddress; + } + CharSequence nextCharSequence = currentlyOutgonigTopLevelStreamElement.toXML(StreamOpen.CLIENT_NAMESPACE); + if (nextCharSequence instanceof XmlStringBuilder) { + XmlStringBuilder xmlStringBuilder = (XmlStringBuilder) nextCharSequence; + outgoingCharSequenceIterator = xmlStringBuilder.getCharSequenceIterator(); + } else { + outgoingCharSequenceIterator = Collections.singletonList(nextCharSequence).iterator(); + } + assert (outgoingCharSequenceIterator != null); + } else { + // There is nothing more to write. + break; + } + } + + pendingOutputFilterData = newPendingOutputFilterData; + if (!pendingWriteInterestAfterRead && pendingOutputFilterData) { + newInterestedOps |= SelectionKey.OP_WRITE; + } + + readLoop: while (true) { + // Prevent one callback from dominating the reactor thread. Break out of the read-loop if we have + // read a certain amount. + if (callbackBytesRead > CALLBACK_MAX_BYTES_READ) { + callbackPreemtBecauseBytesRead++; + break; + } + + int bytesRead; + incomingBuffer.clear(); + try { + bytesRead = selectedSocketChannel.read(incomingBuffer); + } catch (IOException e) { + handleReadWriteIoException(e); + return; + } + + if (bytesRead < 0) { + LOGGER.finer("NIO read() returned " + bytesRead + + " for " + this + ". This probably means that the TCP connection was terminated."); + // According to the socket channel javadoc section about "asynchronous reads" a socket channel's + // read() may return -1 if the input side of a socket is shut down. + + // Note that we do not call notifyConnectionError() here because the connection may be + // cleanly shutdown which would also cause read() to return '-1. I assume that this socket + // will be selected again, on which read() would throw an IOException, which will be catched + // and invoke notifyConnectionError() (see a few lines above). + /* + IOException exception = new IOException("NIO read() returned " + bytesRead); + notifyConnectionError(exception); + */ + return; + } + + if (!pendingInputFilterData) { + if (bytesRead == 0) { + // Nothing more to read. + break; + } + } else { + pendingInputFilterData = false; + } + + // We have successfully read something. It is now possible that a filter is now also able to write + // additional data (for example SSLEngine). + if (pendingWriteInterestAfterRead) { + pendingWriteInterestAfterRead = false; + newInterestedOps |= SelectionKey.OP_WRITE; + } + + callbackBytesRead += bytesRead; + + ByteBuffer filteredIncomingBuffer = incomingBuffer; + for (ListIterator it = getXmppInputOutputFilterEndIterator(); it.hasPrevious();) { + filteredIncomingBuffer.flip(); + + ByteBuffer newFilteredIncomingBuffer; + try { + newFilteredIncomingBuffer = it.previous().input(filteredIncomingBuffer); + } catch (IOException e) { + notifyConnectionError(e); + return; + } + if (newFilteredIncomingBuffer == null) { + break readLoop; + } + filteredIncomingBuffer = newFilteredIncomingBuffer; + } + + final int bytesReadAfterFilter = filteredIncomingBuffer.flip().remaining(); + + totalBytesReadAfterFilter += bytesReadAfterFilter; + + try { + splitter.write(filteredIncomingBuffer); + } catch (IOException e) { + notifyConnectionError(e); + return; + } + } + } finally { + totalBytesWritten += callbackBytesWritten; + totalBytesRead += callbackBytesRead; + + channelSelectedCallbackLock.unlock(); + } + + // Indicate that there is no reactor thread racing towards handling this selection key. + final SelectionKeyAttachment selectionKeyAttachment = this.selectionKeyAttachment; + if (selectionKeyAttachment != null) { + selectionKeyAttachment.resetReactorThreadRacing(); + } + + // Check the queue again to prevent lost wakeups caused by elements inserted before we + // called resetReactorThreadRacing() a few lines above. + if (!outgoingElementsQueue.isEmpty()) { + setWriteInterestAfterChannelSelectedCallback.incrementAndGet(); + newInterestedOps |= SelectionKey.OP_WRITE; + } + + setInterestOps(selectionKey, newInterestedOps); + }; + + private void handleReadWriteIoException(IOException e) { + if (e instanceof ClosedChannelException && !isConnected()) { + // The connection is already closed. + return; + } + + notifyConnectionError(e); + } + + private void callChannelSelectedCallback(boolean setPendingInputFilterData, boolean setPendingOutputFilterData) { + final SocketChannel channel = socketChannel; + final SelectionKey key = selectionKey; + if (channel == null || key == null) { + LOGGER.info("Not calling channel selected callback because the connection was eventually disconnected"); + return; + } + + channelSelectedCallbackLock.lock(); + try { + // Note that it is important that we send the pending(Input|Output)FilterData flags while holding the lock. + if (setPendingInputFilterData) { + pendingInputFilterData = true; + } + if (setPendingOutputFilterData) { + pendingOutputFilterData = true; + } + + channelSelectedCallback.onChannelSelected(channel, key); + } finally { + channelSelectedCallbackLock.unlock(); + } + } + + private abstract static class TcpHostEvent extends DetailedTransitionIntoInformation { + protected final InetSocketAddress inetSocketAddress; + + protected TcpHostEvent(State state, InetSocketAddress inetSocketAddress) { + super(state); + this.inetSocketAddress = inetSocketAddress; + } + + public InetSocketAddress getInetSocketAddress() { + return inetSocketAddress; + } + + @Override + public String toString() { + return super.toString() + ": " + inetSocketAddress; + } + } + + public static final class ConnectingToHostEvent extends TcpHostEvent { + private ConnectingToHostEvent(State state, InetSocketAddress inetSocketAddress) { + super(state, inetSocketAddress); + } + } + + public static final class ConnectedToHostEvent extends TcpHostEvent { + private final boolean connectionEstablishedImmediately; + + private ConnectedToHostEvent(State state, InetSocketAddress inetSocketAddress, boolean immediately) { + super(state, inetSocketAddress); + this.connectionEstablishedImmediately = immediately; + } + + @Override + public String toString() { + return super.toString() + (connectionEstablishedImmediately ? "" : " not") + " connected immediately"; + } + } + + public static final class ConnectionToHostFailedEvent extends TcpHostEvent { + private final IOException ioException; + + private ConnectionToHostFailedEvent(State state, InetSocketAddress inetSocketAddress, IOException ioException) { + super(state, inetSocketAddress); + this.ioException = ioException; + } + + @Override + public String toString() { + return super.toString() + ioException; + } + } + + private final class ConnectionAttemptState { + private final ConnectingToHostState connectingToHostState; + InetSocketAddress inetSocketAddress; + // TODO: Check if we can re-use the socket channel in case some InetSocketAddress fail to connect to. + final SocketChannel socketChannel; + final Iterator remainingAddresses; + final List failedAddresses; + final SynchronizationPoint tcpConnectionEstablishedSyncPoint; + + private ConnectionAttemptState(List inetSocketAddresses, List failedAddresses, + ConnectingToHostState connectingToHostState) throws IOException { + socketChannel = SocketChannel.open(); + socketChannel.configureBlocking(false); + remainingAddresses = inetSocketAddresses.iterator(); + inetSocketAddress = remainingAddresses.next(); + this.failedAddresses = failedAddresses; + this.connectingToHostState = connectingToHostState; + + tcpConnectionEstablishedSyncPoint = new SynchronizationPoint<>(XmppNioTcpConnection.this, + "TCP connection establishment"); + } + + private void establishTcpConnection() { + ConnectingToHostEvent connectingToHostEvent = new ConnectingToHostEvent(connectingToHostState, inetSocketAddress); + invokeConnectionStateMachineListener(connectingToHostEvent); + + final boolean connected; + try { + connected = socketChannel.connect(inetSocketAddress); + } catch (IOException e) { + onIOExceptionWhenEstablishingTcpConnection(e); + return; + } + + if (connected) { + ConnectedToHostEvent connectedToHostEvent = new ConnectedToHostEvent(connectingToHostState, + inetSocketAddress, true); + invokeConnectionStateMachineListener(connectedToHostEvent); + + tcpConnectionEstablishedSyncPoint.reportSuccess(); + return; + } + + try { + registerWithSelector(socketChannel, SelectionKey.OP_CONNECT, + (selectedChannel, selectedSelectionKey) -> { + SocketChannel selectedSocketChannel = (SocketChannel) selectedChannel; + + boolean finishConnected; + try { + finishConnected = selectedSocketChannel.finishConnect(); + } catch (IOException e) { + Async.go(() -> onIOExceptionWhenEstablishingTcpConnection(e)); + return; + } + + if (!finishConnected) { + Async.go(() -> onIOExceptionWhenEstablishingTcpConnection(new IOException("finishConnect() failed"))); + return; + } + + ConnectedToHostEvent connectedToHostEvent = new ConnectedToHostEvent(connectingToHostState, inetSocketAddress, false); + invokeConnectionStateMachineListener(connectedToHostEvent); + + // 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); + } + } + + private void onIOExceptionWhenEstablishingTcpConnection(IOException exception) { + if (!remainingAddresses.hasNext()) { + ConnectionException connectionException = ConnectionException.from(failedAddresses); + tcpConnectionEstablishedSyncPoint.reportFailure(connectionException); + return; + } + + tcpConnectionEstablishedSyncPoint.resetTimeout(); + + HostAddress failedHostAddress = new HostAddress(inetSocketAddress, exception); + failedAddresses.add(failedHostAddress); + + ConnectionToHostFailedEvent connectionToHostFailedEvent = new ConnectionToHostFailedEvent( + connectingToHostState, inetSocketAddress, exception); + invokeConnectionStateMachineListener(connectionToHostFailedEvent); + + inetSocketAddress = remainingAddresses.next(); + + establishTcpConnection(); + } + } + + @Override + protected void connectInternal() throws SmackException, IOException, XMPPException, InterruptedException { + // TODO: Check if those initialization methods can be invoked later. + outgoingElementsQueue.start(); + closingStreamReceived.init(); + + WalkStateGraphContext walkStateGraphContext = buildNewWalkTo(ConnectedButUnauthenticatedStateDescriptor.class) + .build(); + + walkStateGraph(walkStateGraphContext); + } + + private List failedAddresses; + private List inetSocketAddresses; + + private static final class LookupHostAddressesStateDescriptor extends StateDescriptor { + private LookupHostAddressesStateDescriptor() { + super(LookupHostAddressesState.class); + addPredeccessor(DisconnectedStateDescriptor.class); + addSuccessor(ConnectingToHostStateDescriptor.class); + addSuccessor(DirectTlsConnectionToHostStateDescriptor.class); + } + } + + private final class LookupHostAddressesState extends State { + private LookupHostAddressesState(StateDescriptor stateDescriptor) { + super(stateDescriptor); + } + + @Override + protected TransitionIntoResult transitionInto(WalkStateGraphContext walkStateGraphContext) throws ConnectionException { + failedAddresses = populateHostAddresses(); + if (hostAddresses.isEmpty()) { + throw ConnectionException.from(failedAddresses); + } + + inetSocketAddresses = new ArrayList<>(2 * hostAddresses.size()); + for (HostAddress hostAddress : XmppNioTcpConnection.this.hostAddresses) { + List inetAddresses = hostAddress.getInetAddresses(); + for (InetAddress inetAddress : inetAddresses) { + InetSocketAddress inetSocketAddress = new InetSocketAddress(inetAddress, hostAddress.getPort()); + inetSocketAddresses.add(inetSocketAddress); + } + } + + return new HostLookupResult(inetSocketAddresses); + } + + @Override + protected void resetState() { + failedAddresses = null; + inetSocketAddresses = null; + } + } + + public static final class HostLookupResult extends TransitionSuccessResult { + private final List remoteAddresses; + + private HostLookupResult(List remoteAddresses) { + super("Host lookup yielded the following addressess: " + remoteAddresses); + + List remoteAddressesLocal = new ArrayList<>(remoteAddresses.size()); + remoteAddressesLocal.addAll(remoteAddresses); + this.remoteAddresses = Collections.unmodifiableList(remoteAddressesLocal); + } + + public List getRemoteAddresses() { + return remoteAddresses; + } + } + + private static final class DirectTlsConnectionToHostStateDescriptor extends StateDescriptor { + private DirectTlsConnectionToHostStateDescriptor() { + super(DirectTlsConnectionToHostState.class, 368, StateDescriptor.Property.notImplemented); + addPredeccessor(LookupHostAddressesStateDescriptor.class); + addSuccessor(ConnectedButUnauthenticatedStateDescriptor.class); + declarePrecedenceOver(ConnectingToHostStateDescriptor.class); + } + } + + private final class DirectTlsConnectionToHostState extends State { + private DirectTlsConnectionToHostState(StateDescriptor stateDescriptor) { + super(stateDescriptor); + } + + @Override + protected TransitionImpossibleReason isTransitionToPossible(WalkStateGraphContext walkStateGraphContext) { + if (!useDirectTls) { + return new TransitionImpossibleReason("Direct TLS not enabled"); + } + + // TODO: Check if lookup yielded any xmpps SRV RRs. + + throw new IllegalStateException("Direct TLS not implemented"); + } + + @Override + protected TransitionIntoResult transitionInto(WalkStateGraphContext walkStateGraphContext) { + throw new IllegalStateException("Direct TLS not implemented"); + } + } + + private static final class ConnectingToHostStateDescriptor extends StateDescriptor { + private ConnectingToHostStateDescriptor() { + super(ConnectingToHostState.class); + addSuccessor(EstablishTlsStateDescriptor.class); + addSuccessor(ConnectedButUnauthenticatedStateDescriptor.class); + } + } + + private final class ConnectingToHostState extends State { + private ConnectingToHostState(StateDescriptor stateDescriptor) { + super(stateDescriptor); + } + + @Override + protected TransitionIntoResult transitionInto(WalkStateGraphContext walkStateGraphContext) + throws IOException, InterruptedException, NoResponseException, ConnectionException, + ConnectionUnexpectedTerminatedException, NotConnectedException { + // The fields inetSocketAddress and failedAddresses are handed over from LookupHostAddresses to + // ConnectingToHost. + ConnectionAttemptState connectionAttemptState = new ConnectionAttemptState(inetSocketAddresses, + failedAddresses, this); + connectionAttemptState.establishTcpConnection(); + + connectionAttemptState.tcpConnectionEstablishedSyncPoint.checkIfSuccessOrWaitOrThrow(); + + socketChannel = connectionAttemptState.socketChannel; + remoteAddress = (InetSocketAddress) socketChannel.socket().getRemoteSocketAddress(); + + selectionKey = registerWithSelector(socketChannel, SelectionKey.OP_READ, channelSelectedCallback); + selectionKeyAttachment = (SelectionKeyAttachment) selectionKey.attachment(); + + newStreamOpenWaitForFeaturesSequence("stream features after initial connection"); + + return new TcpSocketConnectedResult(remoteAddress); + } + + @Override + protected void resetState() { + cleanUpSelectionKeyAndSocketChannel(); + } + } + + public static final class TcpSocketConnectedResult extends TransitionSuccessResult { + private final InetSocketAddress remoteAddress; + + private TcpSocketConnectedResult(InetSocketAddress remoteAddress) { + super("TCP connection established to " + remoteAddress); + this.remoteAddress = remoteAddress; + } + + public InetSocketAddress getRemoteAddress() { + return remoteAddress; + } + } + + private static final class EstablishTlsStateDescriptor extends StateDescriptor { + private EstablishTlsStateDescriptor() { + super(EstablishTlsState.class, "RFC 6120 ยง 5"); + addSuccessor(ConnectedButUnauthenticatedStateDescriptor.class); + declarePrecedenceOver(ConnectedButUnauthenticatedStateDescriptor.class); + } + } + + private final class EstablishTlsState extends State { + private EstablishTlsState(StateDescriptor stateDescriptor) { + super(stateDescriptor); + } + + @Override + protected TransitionImpossibleReason isTransitionToPossible(WalkStateGraphContext walkStateGraphContext) + throws SecurityRequiredByClientException, SecurityRequiredByServerException { + StartTls startTlsFeature = getFeature(StartTls.ELEMENT, StartTls.NAMESPACE); + SecurityMode securityMode = config.getSecurityMode(); + + switch (securityMode) { + case required: + case ifpossible: + if (startTlsFeature == null) { + if (securityMode == SecurityMode.ifpossible) { + return new TransitionImpossibleReason("Server does not announce support for TLS and we do not required it"); + } + throw new SecurityRequiredByClientException(); + } + // Allows transition by returning null. + return null; + case disabled: + if (startTlsFeature != null && startTlsFeature.required()) { + throw new SecurityRequiredByServerException(); + } + return new TransitionImpossibleReason("TLS disabled in client settings and server does not require it"); + default: + throw new AssertionError("Unknown security mode: " + securityMode); + } + } + + @Override + protected TransitionIntoResult transitionInto(WalkStateGraphContext walkStateGraphContext) + throws SmackException, FailedNonzaException, IOException, InterruptedException { + sendAndWaitForResponse(StartTls.INSTANCE, TlsProceed.class, TlsFailure.class); + + SmackTlsContext smackTlsContext; + try { + smackTlsContext = getSmackTlsContext(); + } catch (KeyManagementException | UnrecoverableKeyException | NoSuchAlgorithmException + | CertificateException | KeyStoreException | NoSuchProviderException e) { + throw new SmackException(e); + } + + tlsState = new TlsState(smackTlsContext); + addXmppInputOutputFilter(tlsState); + + channelSelectedCallbackLock.lock(); + try { + pendingOutputFilterData = true; + // The beginHandshake() is possibly not really required here, but it does not hurt either. + tlsState.engine.beginHandshake(); + tlsState.handshakeStatus = TlsHandshakeStatus.initiated; + } finally { + channelSelectedCallbackLock.unlock(); + } + setInterestOps(selectionKey, SelectionKey.OP_WRITE | SelectionKey.OP_READ); + + try { + tlsState.waitForHandshakeFinished(); + } catch (CertificateException e) { + throw new SmackException(e); + } + + newStreamOpenWaitForFeaturesSequence("stream features after TLS established"); + + return new TlsEstablishedResult(tlsState.engine); + } + + @Override + protected void resetState() { + tlsState = null; + } + } + + public static final class TlsEstablishedResult extends TransitionSuccessResult { + + private TlsEstablishedResult(SSLEngine sslEngine) { + super("TLS established: " + sslEngine.getSession()); + } + } + + private enum TlsHandshakeStatus { + initial, + initiated, + successful, + failed, + ; + } + + private static final Level SSL_ENGINE_DEBUG_LOG_LEVEL = Level.FINEST; + + private static void debugLogSslEngineResult(String operation, SSLEngineResult result) { + if (!LOGGER.isLoggable(SSL_ENGINE_DEBUG_LOG_LEVEL)) { + return; + } + + LOGGER.log(SSL_ENGINE_DEBUG_LOG_LEVEL, "SSLEngineResult of " + operation + "(): " + result); + } + + private final class TlsState implements XmppInputOutputFilter { + + private static final int MAX_PENDING_OUTPUT_BYTES = 8096; + + private final SmackTlsContext smackTlsContext; + private final SSLEngine engine; + + private TlsHandshakeStatus handshakeStatus = TlsHandshakeStatus.initial; + private SSLException handshakeException; + + private ByteBuffer myNetData; + private ByteBuffer peerAppData; + + private final List pendingOutputData = new ArrayList<>(); + private int pendingOutputBytes; + private ByteBuffer pendingInputData; + + private final AtomicInteger pendingDelegatedTasks = new AtomicInteger(); + + private long wrapInBytes; + private long wrapOutBytes; + + private long unwrapInBytes; + private long unwrapOutBytes; + + private TlsState(SmackTlsContext smackTlsContext) throws IOException { + this.smackTlsContext = smackTlsContext; + + // Call createSSLEngine()'s variant with two parameters as this allows for TLS session resumption. + + // Note that it is not really clear what the value of peer host should be. It could be A) the XMPP service's + // domainpart or B) the DNS name of the host we are connecting to (usually the DNS SRV RR target name). While + // the javadoc of createSSLEngine(String, int) indicates with "Some cipher suites (such as Kerberos) require + // remote hostname information, in which case peerHost needs to be specified." that A should be used. TLS + // session resumption may would need or at least benefit from B. Variant A would also be required if the + // String is used for certificate verification. And it appears at least likely that TLS session resumption + // would not be hurt by using variant A. Therfore we currently use variant A. + engine = smackTlsContext.sslContext.createSSLEngine(config.getXMPPServiceDomain().toString(), remoteAddress.getPort()); + engine.setUseClientMode(true); + + SSLSession session = engine.getSession(); + int applicationBufferSize = session.getApplicationBufferSize(); + int packetBufferSize = session.getPacketBufferSize(); + + myNetData = ByteBuffer.allocateDirect(packetBufferSize); + peerAppData = ByteBuffer.allocate(applicationBufferSize); + } + + @Override + public OutputResult output(ByteBuffer outputData, boolean isFinalDataOfElement, boolean destinationAddressChanged, + boolean moreDataAvailable) throws SSLException { + if (outputData != null) { + pendingOutputData.add(outputData); + pendingOutputBytes += outputData.remaining(); + if (moreDataAvailable && pendingOutputBytes < MAX_PENDING_OUTPUT_BYTES) { + return OutputResult.NO_OUTPUT; + } + } + + ByteBuffer[] outputDataArray = pendingOutputData.toArray(new ByteBuffer[pendingOutputData.size()]); + + myNetData.clear(); + + while (true) { + SSLEngineResult result; + try { + result = engine.wrap(outputDataArray, myNetData); + } catch (SSLException e) { + handleSslException(e); + throw e; + } + + debugLogSslEngineResult("wrap", result); + + SSLEngineResult.Status engineResultStatus = result.getStatus(); + + pendingOutputBytes -= result.bytesConsumed(); + + if (engineResultStatus == SSLEngineResult.Status.OK) { + wrapInBytes += result.bytesConsumed(); + wrapOutBytes += result.bytesProduced(); + + SSLEngineResult.HandshakeStatus handshakeStatus = handleHandshakeStatus(result); + switch (handshakeStatus) { + case NEED_UNWRAP: + // NEED_UNWRAP means that we need to receive something in order to continue the handshake. The + // standard channelSelectedCallback logic will take care of this, as there is eventually always + // a interest to read from the socket. + break; + case NEED_WRAP: + // Same as need task: Cycle the reactor. + case NEED_TASK: + // Note that we also set pendingOutputFilterData in the OutputResult in the NEED_TASK case, as + // we also want to retry the wrap() operation above in this case. + return new OutputResult(true, myNetData); + default: + break; + } + } + + switch (engineResultStatus) { + case OK: + // No need to outputData.compact() here, since we do not reuse the buffer. + // Clean up the pending output data. + pruneBufferList(pendingOutputData); + return new OutputResult(!pendingOutputData.isEmpty(), myNetData); + case CLOSED: + pendingOutputData.clear(); + return OutputResult.NO_OUTPUT; + case BUFFER_OVERFLOW: + LOGGER.warning("SSLEngine status BUFFER_OVERFLOW, this is hopefully uncommon"); + int outputDataRemaining = outputData != null ? outputData.remaining() : 0; + int newCapacity = (int) (1.3 * outputDataRemaining); + // If newCapacity would not increase myNetData, then double it. + if (newCapacity <= myNetData.capacity()) { + newCapacity = 2 * myNetData.capacity(); + } + ByteBuffer newMyNetData = ByteBuffer.allocateDirect(newCapacity); + myNetData.flip(); + newMyNetData.put(myNetData); + myNetData = newMyNetData; + continue; + case BUFFER_UNDERFLOW: + throw new IllegalStateException( + "Buffer underflow as result of SSLEngine.wrap() should never happen"); + } + } + } + + @Override + public ByteBuffer input(ByteBuffer inputData) throws SSLException { + ByteBuffer accumulatedData; + if (pendingInputData == null) { + accumulatedData = inputData; + } else { + int accumulatedDataBytes = pendingInputData.remaining() + inputData.remaining(); + accumulatedData = ByteBuffer.allocate(accumulatedDataBytes); + accumulatedData.put(pendingInputData) + .put(inputData) + .flip(); + pendingInputData = null; + } + + peerAppData.clear(); + + while (true) { + SSLEngineResult result; + try { + result = engine.unwrap(accumulatedData, peerAppData); + } catch (SSLException e) { + handleSslException(e); + throw e; + } + + debugLogSslEngineResult("unwrap", result); + + SSLEngineResult.Status engineResultStatus = result.getStatus(); + + if (engineResultStatus == SSLEngineResult.Status.OK) { + unwrapInBytes += result.bytesConsumed(); + unwrapOutBytes += result.bytesProduced(); + + SSLEngineResult.HandshakeStatus handshakeStatus = handleHandshakeStatus(result); + switch (handshakeStatus) { + case NEED_TASK: + // A delegated task is asynchronously running. Signal that there is pending input data and + // cycle again through the smack reactor. + addAsPendingInputData(accumulatedData); + break; + case NEED_UNWRAP: + continue; + case NEED_WRAP: + // NEED_WRAP means that the SSLEngine needs to send data, probably without consuming data. + // We exploit here the fact that the channelSelectedCallback is single threaded and that the + // input processing is after the output processing. + asyncGo(() -> callChannelSelectedCallback(false, true)); + break; + default: + break; + } + } + + switch (engineResultStatus) { + case OK: + // SSLEngine's unwrap() may not consume all bytes from the source buffer. If this is the case, then + // simply perform another unwrap until accumlatedData has no remaining bytes. + if (accumulatedData.hasRemaining()) { + continue; + } + return peerAppData; + case CLOSED: + return null; + case BUFFER_UNDERFLOW: + // There were not enough source bytes available to make a complete packet. Let it in + // pendingInputData. Note that we do not resize SSLEngine's source buffer - inputData in our case - + // as it is not possible. + addAsPendingInputData(accumulatedData); + return null; + case BUFFER_OVERFLOW: + int applicationBufferSize = engine.getSession().getApplicationBufferSize(); + assert (peerAppData.remaining() < applicationBufferSize); + peerAppData = ByteBuffer.allocate(applicationBufferSize); + continue; + } + } + } + + private void addAsPendingInputData(ByteBuffer byteBuffer) { + pendingInputData = ByteBuffer.allocate(byteBuffer.remaining()); + pendingInputData.put(byteBuffer).flip(); + } + + private SSLEngineResult.HandshakeStatus handleHandshakeStatus(SSLEngineResult sslEngineResult) { + SSLEngineResult.HandshakeStatus handshakeStatus = sslEngineResult.getHandshakeStatus(); + switch (handshakeStatus) { + case NEED_TASK: + while (true) { + final Runnable delegatedTask = engine.getDelegatedTask(); + if (delegatedTask == null) { + break; + } + sslEngineDelegatedTasks++; + int currentPendingDelegatedTasks = pendingDelegatedTasks.incrementAndGet(); + if (currentPendingDelegatedTasks > maxPendingSslEngineDelegatedTasks) { + maxPendingSslEngineDelegatedTasks = currentPendingDelegatedTasks; + } + + Runnable wrappedDelegatedTask = () -> { + delegatedTask.run(); + int wrappedCurrentPendingDelegatedTasks = pendingDelegatedTasks.decrementAndGet(); + if (wrappedCurrentPendingDelegatedTasks == 0) { + callChannelSelectedCallback(true, true); + } + }; + asyncGo(wrappedDelegatedTask); + } + break; + case FINISHED: + onHandshakeFinished(); + break; + default: + break; + } + + SSLEngineResult.HandshakeStatus afterHandshakeStatus = engine.getHandshakeStatus(); + return afterHandshakeStatus; + } + + private void handleSslException(SSLException e) { + handshakeException = e; + handshakeStatus = TlsHandshakeStatus.failed; + synchronized (this) { + notifyAll(); + } + } + + private void onHandshakeFinished() { + handshakeStatus = TlsHandshakeStatus.successful; + synchronized (this) { + notifyAll(); + } + } + + private boolean isHandshakeFinished() { + return handshakeStatus == TlsHandshakeStatus.successful || handshakeStatus == TlsHandshakeStatus.failed; + } + + private void waitForHandshakeFinished() throws InterruptedException, CertificateException, SSLException, ConnectionUnexpectedTerminatedException, NoResponseException { + final long deadline = System.currentTimeMillis() + getReplyTimeout(); + + synchronized (this) { + while (!isHandshakeFinished() && currentConnectionException == 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(XmppNioTcpConnection.this, "TLS Handshake finsih"); + } + + if (handshakeStatus == TlsHandshakeStatus.failed) { + throw handshakeException; + } + + assert handshakeStatus == TlsHandshakeStatus.successful; + + if (smackTlsContext.daneVerifier != null) { + smackTlsContext.daneVerifier.finish(engine.getSession()); + } + } + + @Override + public Object getStats() { + return new TlsStateStats(this); + } + + @Override + public void closeInputOutput() { + engine.closeOutbound(); + try { + engine.closeInbound(); + } catch (SSLException e) { + LOGGER.log(Level.FINEST, + "SSLException when closing inbound TLS session. This can likely be ignored if a possible truncation attack is suggested." + + " You may want to ask your XMPP server vendor to implement a clean TLS session shutdown sending close_notify after ", + e); + } + } + + @Override + public void waitUntilInputOutputClosed() throws IOException, CertificateException, InterruptedException, + ConnectionUnexpectedTerminatedException, NoResponseException { + waitForHandshakeFinished(); + } + } + + public static final class TlsStateStats { + public final long wrapInBytes; + public final long wrapOutBytes; + public final double wrapRatio; + + public final long unwrapInBytes; + public final long unwrapOutBytes; + public final double unwrapRatio; + + private TlsStateStats(TlsState tlsState) { + wrapOutBytes = tlsState.wrapOutBytes; + wrapInBytes = tlsState.wrapInBytes; + wrapRatio = (double) wrapOutBytes / wrapInBytes; + + unwrapOutBytes = tlsState.unwrapOutBytes; + unwrapInBytes = tlsState.unwrapInBytes; + unwrapRatio = (double) unwrapInBytes / unwrapOutBytes; + } + + private transient String toStringCache; + + @Override + public String toString() { + if (toStringCache != null) { + return toStringCache; + } + + toStringCache = + "wrap-in-bytes: " + wrapInBytes + '\n' + + "wrap-out-bytes: " + wrapOutBytes + '\n' + + "wrap-ratio: " + wrapRatio + '\n' + + "unwrap-in-bytes: " + unwrapInBytes + '\n' + + "unwrap-out-bytes: " + unwrapOutBytes + '\n' + + "unwrap-ratio: " + unwrapRatio + ; + + return toStringCache; + } + } + + protected static final class EnableStreamManagementStateDescriptor extends StateDescriptor { + private EnableStreamManagementStateDescriptor() { + super(EnableStreamManagementState.class, 198, StateDescriptor.Property.notImplemented); + addPredeccessor(ResourceBindingStateDescriptor.class); + addSuccessor(AuthenticatedAndResourceBoundStateDescriptor.class); + declarePrecedenceOver(AuthenticatedAndResourceBoundStateDescriptor.class); + } + } + + private final class EnableStreamManagementState extends State { + private EnableStreamManagementState(StateDescriptor stateDescriptor) { + super(stateDescriptor); + } + + @Override + protected TransitionImpossibleReason isTransitionToPossible(WalkStateGraphContext walkStateGraphContext) { + if (!useSm) { + return new TransitionImpossibleReason("Stream management not enabled"); + } + + throw new IllegalStateException("SM not implemented"); + } + + @Override + protected TransitionIntoResult transitionInto(WalkStateGraphContext walkStateGraphContext) { + throw new IllegalStateException("SM not implemented"); + } + } + + private static final class ResumeStreamStateDescriptor extends StateDescriptor { + private ResumeStreamStateDescriptor() { + super(ResumeStreamState.class, 198, StateDescriptor.Property.notImplemented); + addPredeccessor(AuthenticatedButUnboundStateDescriptor.class); + addSuccessor(AuthenticatedAndResourceBoundStateDescriptor.class); + declarePrecedenceOver(ResourceBindingStateDescriptor.class); + declareInferiortyTo(CompressionStateDescriptor.class); + } + } + + private final class ResumeStreamState extends State { + private ResumeStreamState(StateDescriptor stateDescriptor) { + super(stateDescriptor); + } + + @Override + protected TransitionImpossibleReason isTransitionToPossible(WalkStateGraphContext walkStateGraphContext) { + if (!useSmResumption) { + return new TransitionImpossibleReason("Stream resumption not enabled"); + } + + throw new IllegalStateException("Stream resumptionimplemented"); + } + + @Override + protected TransitionIntoResult transitionInto(WalkStateGraphContext walkStateGraphContext) throws XMPPErrorException, + SASLErrorException, IOException, SmackException, InterruptedException, FailedNonzaException { + throw new IllegalStateException("Stream resumptionimplemented"); + } + } + + private static final class InstantStreamResumptionStateDescriptor extends StateDescriptor { + private InstantStreamResumptionStateDescriptor() { + super(InstantStreamResumptionState.class, 397, StateDescriptor.Property.notImplemented); + addSuccessor(AuthenticatedAndResourceBoundStateDescriptor.class); + addPredeccessor(ConnectedButUnauthenticatedStateDescriptor.class); + declarePrecedenceOver(SaslAuthenticationStateDescriptor.class); + } + } + + private final class InstantStreamResumptionState extends State { + private InstantStreamResumptionState(StateDescriptor stateDescriptor) { + super(stateDescriptor); + } + + @Override + protected TransitionImpossibleReason isTransitionToPossible(WalkStateGraphContext walkStateGraphContext) { + if (!useIsr) { + return new TransitionImpossibleReason("Instant stream resumption not enabled nor implemented"); + } + + throw new IllegalStateException("Instant stream resumption not implemented"); + } + + @Override + protected TransitionIntoResult transitionInto(WalkStateGraphContext walkStateGraphContext) { + throw new IllegalStateException("Instant stream resumption not implemented"); + } + } + + private static final class Bind2StateDescriptor extends StateDescriptor { + private Bind2StateDescriptor() { + super(Bind2State.class, 386, StateDescriptor.Property.notImplemented); + addPredeccessor(ConnectedButUnauthenticatedStateDescriptor.class); + addSuccessor(AuthenticatedAndResourceBoundStateDescriptor.class); + declarePrecedenceOver(SaslAuthenticationStateDescriptor.class); + } + } + + private final class Bind2State extends State { + private Bind2State(StateDescriptor stateDescriptor) { + super(stateDescriptor); + } + + @Override + protected TransitionImpossibleReason isTransitionToPossible(WalkStateGraphContext walkStateGraphContext) { + if (!useBind2) { + return new TransitionImpossibleReason("Bind2 not enabled nor implemented"); + } + + throw new IllegalStateException("Bind2 not implemented"); + } + + @Override + protected TransitionIntoResult transitionInto(WalkStateGraphContext walkStateGraphContext) { + throw new IllegalStateException("Bind2 not implemented"); + } + } + + static final class InstantShutdownStateDescriptor extends StateDescriptor { + private InstantShutdownStateDescriptor() { + super(InstantShutdownState.class); + addSuccessor(CloseConnectionStateDescriptor.class); + addPredeccessor(AuthenticatedAndResourceBoundStateDescriptor.class); + addPredeccessor(ConnectedButUnauthenticatedStateDescriptor.class); + } + } + + private final class InstantShutdownState extends State { + private InstantShutdownState(StateDescriptor stateDescriptor) { + super(stateDescriptor); + } + + @Override + protected TransitionImpossibleReason isTransitionToPossible(WalkStateGraphContext walkStateGraphContext) { + ensureNotOnOurWayToAuthenticatedAndResourceBound(walkStateGraphContext); + return null; + } + + @Override + protected TransitionIntoResult transitionInto(WalkStateGraphContext walkStateGraphContext) { + outgoingElementsQueue.shutdown(); + afterOutgoingElementsQueueModified(); + + return TransitionSuccessResult.EMPTY_INSTANCE; + } + } + + private static final class ShutdownStateDescriptor extends StateDescriptor { + private ShutdownStateDescriptor() { + super(ShutdownState.class); + addSuccessor(CloseConnectionStateDescriptor.class); + addPredeccessor(AuthenticatedAndResourceBoundStateDescriptor.class); + addPredeccessor(ConnectedButUnauthenticatedStateDescriptor.class); + } + } + + private final class ShutdownState extends State { + private ShutdownState(StateDescriptor stateDescriptor) { + super(stateDescriptor); + } + + @Override + protected TransitionImpossibleReason isTransitionToPossible(WalkStateGraphContext walkStateGraphContext) { + ensureNotOnOurWayToAuthenticatedAndResourceBound(walkStateGraphContext); + return null; + } + + @Override + protected TransitionIntoResult transitionInto(WalkStateGraphContext walkStateGraphContext) { + boolean streamCloseIssued = false; + + closingStreamReceived.init(); + + streamCloseIssued = outgoingElementsQueue.offerAndShutdown(StreamClose.INSTANCE); + + afterOutgoingElementsQueueModified(); + + if (streamCloseIssued) { + boolean successfullyReceivedStreamClose = waitForClosingStreamTagFromServer(); + if (successfullyReceivedStreamClose) { + for (Iterator it = getXmppInputOutputFilterBeginIterator(); it.hasNext();) { + XmppInputOutputFilter filter = it.next(); + filter.closeInputOutput(); + } + + // Flush the new state. + pendingInputFilterData = pendingOutputFilterData = true; + afterOutgoingElementsQueueModified(); + + for (Iterator it = getXmppInputOutputFilterBeginIterator(); it.hasNext();) { + XmppInputOutputFilter filter = it.next(); + try { + filter.waitUntilInputOutputClosed(); + } catch (IOException | CertificateException | InterruptedException | SmackException e) { + LOGGER.log(Level.WARNING, "waitUntilInputOutputClosed() threw", e); + } + } + } + } + + return TransitionSuccessResult.EMPTY_INSTANCE; + } + } + + private static final class CloseConnectionStateDescriptor extends StateDescriptor { + private CloseConnectionStateDescriptor() { + super(CloseConnectionState.class); + addSuccessor(DisconnectedStateDescriptor.class); + } + } + + private final class CloseConnectionState extends State { + private CloseConnectionState(StateDescriptor stateDescriptor) { + super(stateDescriptor); + } + + @Override + protected TransitionIntoResult transitionInto(WalkStateGraphContext walkStateGraphContext) { + cleanUpSelectionKeyAndSocketChannel(); + + return TransitionSuccessResult.EMPTY_INSTANCE; + } + } + + @Override + public boolean isSecureConnection() { + final TlsState tlsState = this.tlsState; + return tlsState != null && tlsState.handshakeStatus == TlsHandshakeStatus.successful; + } + + private void sendTopLevelStreamElement(TopLevelStreamElement topLevelStreamElement) + throws InterruptedException { + outgoingElementsQueue.put(topLevelStreamElement); + afterOutgoingElementsQueueModified(); + } + + private void afterOutgoingElementsQueueModified() { + final SelectionKeyAttachment selectionKeyAttachment = this.selectionKeyAttachment; + if (selectionKeyAttachment != null && selectionKeyAttachment.isReactorThreadRacing()) { + // A reactor thread is already racing to the channel selected callback and will take care of this. + reactorThreadAlreadyRacing.incrementAndGet(); + return; + } + + afterOutgoingElementsQueueModifiedSetInterestOps.incrementAndGet(); + + // Add OP_WRITE to the interested Ops, since we have now new things to write. Note that this may cause + // multiple reactor threads to race to the channel selected callback in case we perform this right after + // a select() returned with this selection key in the selected-key set. Hence we use tryLock() in the + // channel selected callback to keep the invariant that only exactly one thread is performing the + // callback. + // Note that we need to perform setInterestedOps() *without* holding the channelSelectedCallbackLock, as + // otherwise the reactor thread racing to the channel selected callback may found the lock still locked, which + // would result in the outgoingElementsQueue not being handled. + setInterestOps(selectionKey, SelectionKey.OP_WRITE | SelectionKey.OP_READ); + } + + @Override + protected void throwNotConnectedExceptionIfAppropriate() throws NotConnectedException { + if (!connected && !isSmResumptionPossible()) { + throw new NotConnectedException(this, "XMPP connection not connected"); + } + } + + @Override + protected void sendStanzaInternal(Stanza stanza) throws NotConnectedException, InterruptedException { + sendTopLevelStreamElement(stanza); + // TODO: Here would be stream management code once this connection type supports it. + } + + @Override + public void sendNonza(Nonza nonza) throws NotConnectedException, InterruptedException { + sendTopLevelStreamElement(nonza); + } + + @Override + protected void shutdown() { + shutdown(false); + } + + @Override + public synchronized void instantShutdown() { + shutdown(true); + } + + private void shutdown(boolean instant) { + Class mandatoryIntermediateState; + if (instant) { + mandatoryIntermediateState = InstantShutdownStateDescriptor.class; + } else { + mandatoryIntermediateState = ShutdownStateDescriptor.class; + } + + WalkStateGraphContext context = buildNewWalkTo(DisconnectedStateDescriptor.class) + .withMandatoryIntermediateState(mandatoryIntermediateState) + .build(); + + try { + walkStateGraph(context); + } catch (XMPPErrorException | SASLErrorException | IOException | SmackException | InterruptedException | FailedNonzaException e) { + throw new IllegalStateException("A walk to disconnected state should never throw", e); + } + } + + private void cleanUpSelectionKeyAndSocketChannel() { + final SelectionKey selectionKey = this.selectionKey; + if (selectionKey != null) { + selectionKey.cancel(); + } + final SocketChannel socketChannel = this.socketChannel; + if (socketChannel != null) { + try { + socketChannel.close(); + } catch (IOException e) { + + } + } + + this.selectionKey = null; + this.socketChannel = null; + + selectionKeyAttachment = null; + remoteAddress = null; + } + + public boolean isSmResumptionPossible() { + return false; + } + + public Stats getStats() { + return new Stats(this); + } + + public static final class Stats { + public final long totalBytesWritten; + public final long totalBytesWrittenBeforeFilter; + public final double writeRatio; + + public final long totalBytesRead; + public final long totalBytesReadAfterFilter; + public final double readRatio; + + public final long handledChannelSelectedCallbacks; + public final long setWriteInterestAfterChannelSelectedCallback; + public final long reactorThreadAlreadyRacing; + public final long afterOutgoingElementsQueueModifiedSetInterestOps; + public final long rejectedChannelSelectedCallbacks; + public final long totalCallbackRequests; + public final long callbackPreemtBecauseBytesWritten; + public final long callbackPreemtBecauseBytesRead; + public final int sslEngineDelegatedTasks; + public final int maxPendingSslEngineDelegatedTasks; + public final List filterStats; + + private Stats(XmppNioTcpConnection connection) { + totalBytesWritten = connection.totalBytesWritten; + totalBytesWrittenBeforeFilter = connection.totalBytesWrittenBeforeFilter; + writeRatio = (double) totalBytesWritten / totalBytesWrittenBeforeFilter; + + totalBytesReadAfterFilter = connection.totalBytesReadAfterFilter; + totalBytesRead = connection.totalBytesRead; + readRatio = (double) totalBytesRead / totalBytesReadAfterFilter; + + handledChannelSelectedCallbacks = connection.handledChannelSelectedCallbacks; + setWriteInterestAfterChannelSelectedCallback = connection.setWriteInterestAfterChannelSelectedCallback.get(); + reactorThreadAlreadyRacing = connection.reactorThreadAlreadyRacing.get(); + afterOutgoingElementsQueueModifiedSetInterestOps = connection.afterOutgoingElementsQueueModifiedSetInterestOps + .get(); + rejectedChannelSelectedCallbacks = connection.rejectedChannelSelectedCallbacks.get(); + + totalCallbackRequests = handledChannelSelectedCallbacks + rejectedChannelSelectedCallbacks; + + callbackPreemtBecauseBytesRead = connection.callbackPreemtBecauseBytesRead; + callbackPreemtBecauseBytesWritten = connection.callbackPreemtBecauseBytesWritten; + + sslEngineDelegatedTasks = connection.sslEngineDelegatedTasks; + maxPendingSslEngineDelegatedTasks = connection.maxPendingSslEngineDelegatedTasks; + + filterStats = connection.getFilterStats(); + } + + private transient String toStringCache; + + @Override + public String toString() { + if (toStringCache != null) { + return toStringCache; + } + + StringBuilder sb = new StringBuilder( + "Total bytes\n" + + "recv: " + totalBytesRead + '\n' + + "send: " + totalBytesWritten + '\n' + + "recv-aft-filter: " + totalBytesReadAfterFilter + '\n' + + "send-bef-filter: " + totalBytesWrittenBeforeFilter + '\n' + + "read-ratio: " + readRatio + '\n' + + "write-ratio: " + writeRatio + '\n' + + "Events\n" + + "total-callback-requests: " + totalCallbackRequests + '\n' + + "handled-channel-selected-callbacks: " + handledChannelSelectedCallbacks + '\n' + + "rejected-channel-selected-callbacks: " + rejectedChannelSelectedCallbacks + '\n' + + "set-write-interest-after-callback: " + setWriteInterestAfterChannelSelectedCallback + '\n' + + "reactor-thread-already-racing: " + reactorThreadAlreadyRacing + '\n' + + "after-queue-modified-set-interest-ops: " + afterOutgoingElementsQueueModifiedSetInterestOps + '\n' + + "callback-preemt-because-bytes-read: " + callbackPreemtBecauseBytesRead + '\n' + + "callback-preemt-because-bytes-written: " + callbackPreemtBecauseBytesWritten + '\n' + + "ssl-engine-delegated-tasks: " + sslEngineDelegatedTasks + '\n' + + "max-pending-ssl-engine-delegated-tasks: " + maxPendingSslEngineDelegatedTasks + '\n' + ); + + if (!filterStats.isEmpty()) { + sb.append("Filter Stats\n"); + for (Object filterStat : filterStats) { + sb.append(filterStat); + } + } + + toStringCache = sb.toString(); + + return toStringCache; + } + } + + private static List pruneBufferList(Collection buffers) { + return CollectionUtil.removeUntil(buffers, b -> b.hasRemaining()); + } + + @Override + protected SSLSession getSSLSession() { + if (tlsState == null) { + return null; + } + return tlsState.engine.getSession(); + } + + public static Set> getBackwardEdgesStateDescriptors() { + return Collections.unmodifiableSet(BACKWARD_EDGES_STATE_DESCRIPTORS); + } + +} diff --git a/smack-tcp/src/test/java/org/jivesoftware/smack/tcp/XmppNioTcpConnectionTest.java b/smack-tcp/src/test/java/org/jivesoftware/smack/tcp/XmppNioTcpConnectionTest.java new file mode 100644 index 000000000..14162b3e2 --- /dev/null +++ b/smack-tcp/src/test/java/org/jivesoftware/smack/tcp/XmppNioTcpConnectionTest.java @@ -0,0 +1,32 @@ +/** + * + * Copyright 2018 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.tcp; + +import org.jivesoftware.smack.fsm.StateDescriptor; +import org.jivesoftware.smack.fsm.StateDescriptorGraph.GraphVertex; +import org.jivesoftware.smack.tcp.XmppNioTcpConnection.InstantShutdownStateDescriptor; + +public class XmppNioTcpConnectionTest { + + public void graphComplete() { + assertContains(XmppNioTcpConnection.INITIAL_STATE_DESCRIPTOR_VERTEX, InstantShutdownStateDescriptor.class); + } + + private static void assertContains(GraphVertex graph, Class state) { + throw new Error("Implement me"); + } +} diff --git a/version.gradle b/version.gradle index 2795790a4..f6e8cc09f 100644 --- a/version.gradle +++ b/version.gradle @@ -4,6 +4,6 @@ allprojects { isSnapshot = true jxmppVersion = '0.7.0-alpha5' miniDnsVersion = '0.4.0-alpha3' - smackMinAndroidSdk = 9 + smackMinAndroidSdk = 19 } }