From 842f935bf7d3c94afc72f356b6f862bb28d7c995 Mon Sep 17 00:00:00 2001 From: NikitaZaltsman Date: Sat, 4 Jan 2020 07:29:02 +0700 Subject: [PATCH 01/32] Update kotlin and gradle plugin version --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index d8021700..eff08c4a 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.3.50' + ext.kotlin_version = '1.3.61' ext.dokka_version = '0.9.16' repositories { mavenCentral() @@ -10,7 +10,7 @@ buildscript { } dependencies { classpath 'com.vanniktech:gradle-maven-publish-plugin:0.8.0' - classpath 'com.android.tools.build:gradle:3.4.2' + classpath 'com.android.tools.build:gradle:3.5.3' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } From abec7bc47e907ef5ff1936c3cf04c44b542b41c8 Mon Sep 17 00:00:00 2001 From: NikitaZaltsman Date: Sat, 4 Jan 2020 07:30:16 +0700 Subject: [PATCH 02/32] Increase build tools Increate targetSdk, compileSdk and build tools vesrions. --- dependencies.gradle | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dependencies.gradle b/dependencies.gradle index db1a4143..9668f9b6 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -1,11 +1,11 @@ ext { minSdkVersion = 24 minSdkVersionAndroidLifecycle = 19 - targetSdkVersion = 28 - compileSdkVersion = 28 - buildToolsVersion = '28.0.3' + targetSdkVersion = 29 + compileSdkVersion = 29 + buildToolsVersion = '29.0.0' - appCompat = 'androidx.appcompat:appcompat:1.0.0' + appCompat = 'androidx.appcompat:appcompat:1.1.0' lifecycleExtensions = 'androidx.lifecycle:lifecycle-extensions:2.0.0' material = 'com.google.android.material:material:1.0.0' multiDex = 'androidx.multidex:multidex:2.0.0' From e4d135a92863d41e8f471dec9650c45bf87551c7 Mon Sep 17 00:00:00 2001 From: NikitaZaltsman Date: Wed, 8 Jan 2020 14:53:47 +0600 Subject: [PATCH 03/32] Add stomp server and client command enum --- .../scarlet/stomp/stomp/StompClientCommand.kt | 28 +++++++++++++++++++ .../scarlet/stomp/stomp/StompServerCommand.kt | 16 +++++++++++ 2 files changed, 44 insertions(+) create mode 100644 scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/stomp/StompClientCommand.kt create mode 100644 scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/stomp/StompServerCommand.kt diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/stomp/StompClientCommand.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/stomp/StompClientCommand.kt new file mode 100644 index 00000000..fa230bed --- /dev/null +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/stomp/StompClientCommand.kt @@ -0,0 +1,28 @@ +package com.tinder.scarlet.stomp.stomp + +enum class StompClientCommand { + /** + * + */ + CONNECT, + /** + * + */ + DISCONNECT, + /** + * + */ + SEND, + /** + * + */ + SUBSCRIBE, + /** + * + */ + UNSUBSCRIBE, + /** + * + */ + UNKNOWN +} \ No newline at end of file diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/stomp/StompServerCommand.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/stomp/StompServerCommand.kt new file mode 100644 index 00000000..27acaa20 --- /dev/null +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/stomp/StompServerCommand.kt @@ -0,0 +1,16 @@ +package com.tinder.scarlet.stomp.stomp + +enum class StompServerCommand { + /** + * + */ + CONNECTED, + /** + * + */ + MESSAGE, + /** + * + */ + ERROR +} \ No newline at end of file From 20e63ebbfa88bd734088ac4bc094a8c41afc9703 Mon Sep 17 00:00:00 2001 From: NikitaZaltsman Date: Wed, 8 Jan 2020 14:55:08 +0600 Subject: [PATCH 04/32] Add first okhttp stomp protocol implementation Update deps libs Optimize imports Remove extra --- dependencies.gradle | 24 +++-- scarlet-protocol-stomp/build.gradle | 2 + .../com/tinder/scarlet/stomp/StompEvent.kt | 9 -- .../scarlet/stomp/okhttp/OkHttpStompClient.kt | 52 ++++++++++ .../stomp/okhttp/OkHttpStompDestination.kt | 50 ++++++++++ .../stomp/okhttp/OkHttpStompMainChannel.kt | 97 +++++++++++++++++++ .../stomp/okhttp/OkHttpStompMessageChannel.kt | 59 +++++++++++ .../scarlet/stomp/okhttp/WebSocketFactory.kt | 13 +++ .../scarlet/stomp/stomp/StompClientCommand.kt | 28 ------ .../scarlet/stomp/stomp/StompServerCommand.kt | 16 --- 10 files changed, 289 insertions(+), 61 deletions(-) delete mode 100644 scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/StompEvent.kt create mode 100644 scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompClient.kt create mode 100644 scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompDestination.kt create mode 100644 scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompMainChannel.kt create mode 100644 scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompMessageChannel.kt create mode 100644 scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/WebSocketFactory.kt delete mode 100644 scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/stomp/StompClientCommand.kt delete mode 100644 scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/stomp/StompServerCommand.kt diff --git a/dependencies.gradle b/dependencies.gradle index 9668f9b6..2647406e 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -17,16 +17,22 @@ ext { rxJava3 = 'io.reactivex.rxjava3:rxkotlin:3.0.0' rxAndroid3 = 'io.reactivex.rxjava3:rxandroid:3.0.0' rxKotlin3 = 'io.reactivex.rxjava3:rxkotlin:3.0.0' - rxJava = 'io.reactivex.rxjava2:rxjava:2.1.16' - rxAndroid = 'io.reactivex.rxjava2:rxandroid:2.0.2' - rxKotlin = 'io.reactivex.rxjava2:rxkotlin:2.2.0' + + def rxJavaVersion = "2.2.15" + def rxAndroidVersion = "2.1.1" + def rxKotlinVersion = "2.4.0" + rxJava = "io.reactivex.rxjava2:rxjava:$rxJavaVersion" + rxAndroid = "io.reactivex.rxjava2:rxandroid:$rxAndroidVersion" + rxKotlin = "io.reactivex.rxjava2:rxkotlin:$rxKotlinVersion" rxJava1 = 'io.reactivex:rxjava:1.3.4' + kotlinCoroutines = 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.2' kotlinCoroutinesRxInterop = 'org.jetbrains.kotlinx:kotlinx-coroutines-reactive:1.3.2' kotlinCoroutinesTest = 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.2' - stetho = 'com.facebook.stetho:stetho:1.5.0' - stethoOkHttp = 'com.facebook.stetho:stetho-okhttp3:1.5.0' + def stethoVersion = "1.5.0" + stetho = "com.facebook.stetho:stetho:$stethoVersion" + stethoOkHttp = "com.facebook.stetho:stetho-okhttp3:$stethoVersion" moshi = 'com.squareup.moshi:moshi-kotlin:1.5.0' moshiCodeGen = 'com.squareup.moshi:moshi-kotlin-codegen:1.6.0' @@ -44,9 +50,11 @@ ext { jodaTime = 'joda-time:joda-time:2.9.9' glide = 'com.github.bumptech.glide:glide:4.5.0' - okHttp = 'com.squareup.okhttp3:okhttp:3.11.0' - okHttpServerSentEvent = 'com.squareup.okhttp3:okhttp-sse:3.11.0' - okHttpLoggingInterceptor = 'com.squareup.okhttp3:logging-interceptor:3.11.0' + def okHttpVersion = "3.12.7" + okHttp = "com.squareup.okhttp3:okhttp:$okHttpVersion" + okHttpServerSentEvent = "com.squareup.okhttp3:okhttp-sse:$okHttpVersion" + okHttpLoggingInterceptor = "com.squareup.okhttp3:logging-interceptor:$okHttpVersion" + okio = 'com.squareup.okio:okio:1.13.0' mockWebServer = 'com.squareup.okhttp3:mockwebserver:3.11.0' timber = 'com.jakewharton.timber:timber:4.6.0' diff --git a/scarlet-protocol-stomp/build.gradle b/scarlet-protocol-stomp/build.gradle index 3eb282f1..239d8406 100644 --- a/scarlet-protocol-stomp/build.gradle +++ b/scarlet-protocol-stomp/build.gradle @@ -14,6 +14,8 @@ dependencies { implementation rootProject.ext.rxJava implementation rootProject.ext.kotlinStdlib + api rootProject.ext.okHttp + testImplementation project(':scarlet-test-utils') testImplementation rootProject.ext.junit testImplementation rootProject.ext.mockito diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/StompEvent.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/StompEvent.kt deleted file mode 100644 index 477bfb90..00000000 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/StompEvent.kt +++ /dev/null @@ -1,9 +0,0 @@ -/* - * © 2018 Match Group, LLC. - */ - -package com.tinder.scarlet.stomp - -import com.tinder.scarlet.ProtocolSpecificEvent - -sealed class StompEvent : ProtocolSpecificEvent diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompClient.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompClient.kt new file mode 100644 index 00000000..da0d7d23 --- /dev/null +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompClient.kt @@ -0,0 +1,52 @@ +package com.tinder.scarlet.stomp.okhttp + +import com.tinder.scarlet.Channel +import com.tinder.scarlet.Protocol +import com.tinder.scarlet.ProtocolSpecificEventAdapter +import com.tinder.scarlet.utils.SimpleProtocolOpenRequestFactory +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.WebSocketListener + +class OkHttpStompClient( + private val okHttpClient: OkHttpClient, + private val openRequestFactory: RequestFactory +) : Protocol { + + override fun createChannelFactory(): Channel.Factory { + return OkHttpStompMainChannel.Factory( + object : WebSocketFactory { + override fun createWebSocket(request: Request, listener: WebSocketListener) { + okHttpClient.newWebSocket(request, listener) + } + } + ) + } + + override fun createOpenRequestFactory(channel: Channel): Protocol.OpenRequest.Factory { + return SimpleProtocolOpenRequestFactory { + openRequestFactory.createClientOpenRequest() + } + } + + override fun createEventAdapterFactory(): ProtocolSpecificEventAdapter.Factory { + return object : ProtocolSpecificEventAdapter.Factory {} + } + + interface RequestFactory { + fun createClientOpenRequest(): ClientOpenSocketRequest + } + + open class SimpleRequestFactory( + private val createClientOpenRequestCallable: () -> ClientOpenSocketRequest + ) : RequestFactory { + override fun createClientOpenRequest(): ClientOpenSocketRequest { + return createClientOpenRequestCallable() + } + } + + data class ClientOpenSocketRequest( + val okHttpRequest: Request + ) : Protocol.OpenRequest + +} \ No newline at end of file diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompDestination.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompDestination.kt new file mode 100644 index 00000000..f81750b6 --- /dev/null +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompDestination.kt @@ -0,0 +1,50 @@ +package com.tinder.scarlet.stomp.okhttp + +import com.tinder.scarlet.Channel +import com.tinder.scarlet.Protocol +import com.tinder.scarlet.ProtocolSpecificEventAdapter +import com.tinder.scarlet.utils.SimpleChannelFactory +import com.tinder.scarlet.utils.SimpleProtocolOpenRequestFactory + +class OkHttpStompDestination( + private val destination: String, + private val openRequestFactory: RequestFactory +) : Protocol { + + interface RequestFactory { + + fun createDestinationOpenRequestHeader(destination: String): Map + + } + + override fun createChannelFactory() = SimpleChannelFactory { listener, parent -> + require(parent is OkHttpStompMainChannel) + OkHttpStompMessageChannel(parent, destination, listener) + } + + override fun createOpenRequestFactory(channel: Channel) = SimpleProtocolOpenRequestFactory { + DestinationOpenRequest(openRequestFactory.createDestinationOpenRequestHeader(destination)) + } + + override fun createEventAdapterFactory(): ProtocolSpecificEventAdapter.Factory { + return object : ProtocolSpecificEventAdapter.Factory {} + } + + open class SimpleRequestFactory( + private val createDestinationOpenRequestHeaderCallable: (String) -> Map + ) : RequestFactory { + + override fun createDestinationOpenRequestHeader(destination: String): Map { + return createDestinationOpenRequestHeaderCallable(destination) + } + } + + data class MessageMetaData( + val headers: Map + ) : Protocol.MessageMetaData + + data class DestinationOpenRequest( + val headers: Map + ) : Protocol.OpenRequest + +} \ No newline at end of file diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompMainChannel.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompMainChannel.kt new file mode 100644 index 00000000..5ad4a94a --- /dev/null +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompMainChannel.kt @@ -0,0 +1,97 @@ +package com.tinder.scarlet.stomp.okhttp + +import com.tinder.scarlet.Channel +import com.tinder.scarlet.Protocol +import okhttp3.Response +import okhttp3.WebSocket +import okhttp3.WebSocketListener +import okio.ByteString + +class OkHttpStompMainChannel( + private val webSocketFactory: WebSocketFactory, + private val listener: Channel.Listener +) : Channel { + + private var webSocket: WebSocket? = null + + override fun open(openRequest: Protocol.OpenRequest) { + val openSocketRequest = openRequest as OkHttpStompClient.ClientOpenSocketRequest + webSocketFactory.createWebSocket(openSocketRequest.okHttpRequest, InnerWebSocketListener()) + } + + override fun forceClose() { + TODO("Implement send disconnect message") + webSocket?.cancel() + webSocket = null + } + + override fun close(closeRequest: Protocol.CloseRequest) { + TODO("Implement send disconnect message") + webSocket?.close(1000, "")//todo add code and reason + webSocket = null + } + + fun sendMessage(destination: String, message: String, headers: Map): Boolean { + TODO("Implement send message to destination") + } + + fun subscribe( + destination: String, + headers: Map, + listener: (String, Map) -> Unit + ) { + TODO("Implement subscribe to destination") + } + + fun unSubscribe(destination: String) { + TODO("Implement un subscribe from destination") + } + + inner class InnerWebSocketListener : WebSocketListener() { + override fun onOpen(webSocket: WebSocket, response: Response) { + this@OkHttpStompMainChannel.webSocket = webSocket + TODO("Send connect message") + } + + override fun onMessage(webSocket: WebSocket, bytes: ByteString) { + TODO("Implement handle stomp message") + } + + override fun onMessage(webSocket: WebSocket, text: String) { + TODO("Implement handle stomp message") + } + + override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { + listener.onClosing(this@OkHttpStompMainChannel) + } + + override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { + listener.onClosed(this@OkHttpStompMainChannel) + this@OkHttpStompMainChannel.webSocket = null + } + + override fun onFailure(webSocket: WebSocket, throwable: Throwable, response: Response?) { + listener.onFailed(this@OkHttpStompMainChannel, true, throwable) + this@OkHttpStompMainChannel.webSocket = null + } + } + + class Factory( + private val webSocketFactory: WebSocketFactory + ) : Channel.Factory { + + override fun create( + listener: Channel.Listener, + parent: Channel? + ): Channel? { + return OkHttpStompMainChannel( + webSocketFactory, + listener + ) + } + } + + +} + + diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompMessageChannel.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompMessageChannel.kt new file mode 100644 index 00000000..8014c7b1 --- /dev/null +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompMessageChannel.kt @@ -0,0 +1,59 @@ +package com.tinder.scarlet.stomp.okhttp + +import com.tinder.scarlet.Channel +import com.tinder.scarlet.Message +import com.tinder.scarlet.MessageQueue +import com.tinder.scarlet.Protocol + +class OkHttpStompMessageChannel( + private val mainChannel: OkHttpStompMainChannel, + private val destination: String, + private val listener: Channel.Listener +) : Channel, MessageQueue { + + private var messageQueueListener: MessageQueue.Listener? = null + + override fun open(openRequest: Protocol.OpenRequest) { + val destinationOpenRequest = openRequest as OkHttpStompDestination.DestinationOpenRequest + mainChannel.subscribe( + destination, + destinationOpenRequest.headers + ) { message, headers -> + messageQueueListener?.onMessageReceived( + this, + this, + Message.Text(message), + OkHttpStompDestination.MessageMetaData(headers) + ) + } + listener.onOpened(this) + } + + override fun close(closeRequest: Protocol.CloseRequest) { + forceClose() + } + + override fun forceClose() { + mainChannel.unSubscribe(destination) + listener.onClosed(this) + } + + override fun createMessageQueue(listener: MessageQueue.Listener): MessageQueue { + require(messageQueueListener == null) + messageQueueListener = listener + return this + } + + override fun send(message: Message, messageMetaData: Protocol.MessageMetaData): Boolean { + val headers = messageMetaData as? OkHttpStompDestination.MessageMetaData + return when (message) { + is Message.Text -> mainChannel.sendMessage( + destination, + message.value, + headers?.headers.orEmpty() + ) + is Message.Bytes -> throw IllegalArgumentException("Bytes are not supported") + } + } + +} diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/WebSocketFactory.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/WebSocketFactory.kt new file mode 100644 index 00000000..cea3569c --- /dev/null +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/WebSocketFactory.kt @@ -0,0 +1,13 @@ +/* + * © 2018 Match Group, LLC. + */ + +package com.tinder.scarlet.stomp.okhttp + +import okhttp3.Request +import okhttp3.WebSocketListener + +interface WebSocketFactory { + + fun createWebSocket(request: Request, listener: WebSocketListener) +} diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/stomp/StompClientCommand.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/stomp/StompClientCommand.kt deleted file mode 100644 index fa230bed..00000000 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/stomp/StompClientCommand.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.tinder.scarlet.stomp.stomp - -enum class StompClientCommand { - /** - * - */ - CONNECT, - /** - * - */ - DISCONNECT, - /** - * - */ - SEND, - /** - * - */ - SUBSCRIBE, - /** - * - */ - UNSUBSCRIBE, - /** - * - */ - UNKNOWN -} \ No newline at end of file diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/stomp/StompServerCommand.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/stomp/StompServerCommand.kt deleted file mode 100644 index 27acaa20..00000000 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/stomp/StompServerCommand.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.tinder.scarlet.stomp.stomp - -enum class StompServerCommand { - /** - * - */ - CONNECTED, - /** - * - */ - MESSAGE, - /** - * - */ - ERROR -} \ No newline at end of file From 226d782771ddd84b794bcf6087330fbbac7f37ab Mon Sep 17 00:00:00 2001 From: NikitaZaltsman Date: Thu, 9 Jan 2020 19:20:48 +0600 Subject: [PATCH 05/32] Add stomp core classes Update okhttp stomp client Update stomp core Add message builder. Change stomp message model. Update StompHeaderAccessor. --- .../tinder/scarlet/stomp/core/StompCommand.kt | 21 +++++++ .../tinder/scarlet/stomp/core/StompHeader.kt | 55 +++++++++++++++++++ .../tinder/scarlet/stomp/core/StompMessage.kt | 55 +++++++++++++++++++ .../tinder/scarlet/stomp/core/StompSender.kt | 7 +++ .../scarlet/stomp/core/StompSubscriber.kt | 11 ++++ .../scarlet/stomp/okhttp/OkHttpStompClient.kt | 40 ++++++++++++-- .../stomp/okhttp/OkHttpStompDestination.kt | 12 ++-- .../stomp/okhttp/OkHttpStompMainChannel.kt | 54 ++++++++++++++---- .../stomp/okhttp/OkHttpStompMessageChannel.kt | 35 +++++------- .../stomp/support/StompHeaderAccessor.kt | 55 +++++++++++++++++++ 10 files changed, 302 insertions(+), 43 deletions(-) create mode 100644 scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompCommand.kt create mode 100644 scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompHeader.kt create mode 100644 scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompMessage.kt create mode 100644 scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompSender.kt create mode 100644 scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompSubscriber.kt create mode 100644 scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompHeaderAccessor.kt diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompCommand.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompCommand.kt new file mode 100644 index 00000000..e7ef16f1 --- /dev/null +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompCommand.kt @@ -0,0 +1,21 @@ +package com.tinder.scarlet.stomp.core + +enum class StompCommand( + val isBodyAllowed: Boolean = false, + val isDestinationRequired: Boolean = false +) { + //client + CONNECT, + DISCONNECT, + SEND(isBodyAllowed = true, isDestinationRequired = true), + SUBSCRIBE(isDestinationRequired = true), + UNSUBSCRIBE, + + //server + CONNECTED, + MESSAGE(isBodyAllowed = true, isDestinationRequired = true), + ERROR(isBodyAllowed = true), + + //heartbeat + UNKNOWN +} \ No newline at end of file diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompHeader.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompHeader.kt new file mode 100644 index 00000000..90222266 --- /dev/null +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompHeader.kt @@ -0,0 +1,55 @@ +package com.tinder.scarlet.stomp.core + +class StompHeader( + private val headers: Map +) : Map by headers { + + companion object { + + // Standard headers (as defined in the spec) + const val CONTENT_TYPE = "content-type" // SEND, MESSAGE, ERROR + + const val CONTENT_LENGTH = "content-length" // SEND, MESSAGE, ERROR + + const val RECEIPT = "receipt" // any client frame other than CONNECT + + // CONNECT + const val HOST = "host" + + const val ACCEPT_VERSION = "accept-version" + + const val LOGIN = "login" + + const val PASSCODE = "passcode" + + const val HEARTBEAT = "heart-beat" + + // CONNECTED + + const val SESSION = "session" + + const val SERVER = "server" + + // SEND + + const val DESTINATION = "destination" + + // SUBSCRIBE, UNSUBSCRIBE + const val ID = "id" + + const val ACK = "ack" + + // MESSAGE + const val SUBSCRIPTION = "subscription" + + const val MESSAGE_ID = "message-id" + + // RECEIPT + const val RECEIPT_ID = "receipt-id" + + } + + val destination: String? + get() = headers[DESTINATION] + +} \ No newline at end of file diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompMessage.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompMessage.kt new file mode 100644 index 00000000..9e94c99a --- /dev/null +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompMessage.kt @@ -0,0 +1,55 @@ +package com.tinder.scarlet.stomp.core + +import com.tinder.scarlet.stomp.support.StompHeaderAccessor + +class StompMessage private constructor( + val command: StompCommand, + val payload: String?, + val header: StompHeader +) { + + class Builder { + + private var payload: String? = null + private var headers: StompHeaderAccessor = StompHeaderAccessor.of() + + fun withPayload(payload: String): Builder { + this.payload = payload + return this + } + + fun withHeaders(stompHeader: StompHeader): Builder { + this.headers.putAll(stompHeader) + return this + } + + fun create(command: StompCommand): StompMessage { + val createHeader = headers.createHeader() + if (command.isDestinationRequired) check(!createHeader.destination.isNullOrEmpty()) { "Command $command required destination" } + if (!command.isBodyAllowed) check(payload.isNullOrEmpty()) { "Command $command doesn't support body" } + return StompMessage(command, payload, createHeader) + } + + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as StompMessage + + if (command != other.command) return false + if (payload != other.payload) return false + if (header != other.header) return false + + return true + } + + override fun hashCode(): Int { + var result = command.hashCode() + result = 31 * result + (payload?.hashCode() ?: 0) + result = 31 * result + header.hashCode() + return result + } + +} \ No newline at end of file diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompSender.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompSender.kt new file mode 100644 index 00000000..bc72b8d0 --- /dev/null +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompSender.kt @@ -0,0 +1,7 @@ +package com.tinder.scarlet.stomp.core + +interface StompSender { + + fun convertAndSend(payload: String, destination: String, headers: StompHeader?): Boolean + +} \ No newline at end of file diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompSubscriber.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompSubscriber.kt new file mode 100644 index 00000000..5e094a03 --- /dev/null +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompSubscriber.kt @@ -0,0 +1,11 @@ +package com.tinder.scarlet.stomp.core + +typealias StompListener = (StompMessage) -> Unit + +interface StompSubscriber { + + fun subscribe(destination: String, headers: StompHeader?, listener: StompListener) + + fun unsubscribe(destination: String) + +} \ No newline at end of file diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompClient.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompClient.kt index da0d7d23..f153c6fc 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompClient.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompClient.kt @@ -3,6 +3,8 @@ package com.tinder.scarlet.stomp.okhttp import com.tinder.scarlet.Channel import com.tinder.scarlet.Protocol import com.tinder.scarlet.ProtocolSpecificEventAdapter +import com.tinder.scarlet.stomp.core.StompHeader +import com.tinder.scarlet.utils.SimpleProtocolCloseRequestFactory import com.tinder.scarlet.utils.SimpleProtocolOpenRequestFactory import okhttp3.OkHttpClient import okhttp3.Request @@ -10,7 +12,7 @@ import okhttp3.WebSocketListener class OkHttpStompClient( private val okHttpClient: OkHttpClient, - private val openRequestFactory: RequestFactory + private val requestFactory: RequestFactory ) : Protocol { override fun createChannelFactory(): Channel.Factory { @@ -25,7 +27,13 @@ class OkHttpStompClient( override fun createOpenRequestFactory(channel: Channel): Protocol.OpenRequest.Factory { return SimpleProtocolOpenRequestFactory { - openRequestFactory.createClientOpenRequest() + requestFactory.createClientOpenRequest() + } + } + + override fun createCloseRequestFactory(channel: Channel): Protocol.CloseRequest.Factory { + return SimpleProtocolCloseRequestFactory { + requestFactory.createClientCloseRequest() } } @@ -34,19 +42,39 @@ class OkHttpStompClient( } interface RequestFactory { - fun createClientOpenRequest(): ClientOpenSocketRequest + fun createClientCloseRequest(): ClientCloseRequest + fun createClientOpenRequest(): ClientOpenRequest } open class SimpleRequestFactory( - private val createClientOpenRequestCallable: () -> ClientOpenSocketRequest + private val createClientCloseRequestCallable: () -> ClientCloseRequest, + private val createClientOpenRequestCallable: () -> ClientOpenRequest ) : RequestFactory { - override fun createClientOpenRequest(): ClientOpenSocketRequest { + + override fun createClientCloseRequest(): ClientCloseRequest { + return createClientCloseRequestCallable() + } + + override fun createClientOpenRequest(): ClientOpenRequest { return createClientOpenRequestCallable() } } - data class ClientOpenSocketRequest( + data class ClientOpenRequest( + val host: String, + val login: String? = null, + val passcode: String? = null, val okHttpRequest: Request ) : Protocol.OpenRequest + data class ClientCloseRequest( + val code: Int, + val reason: String + ) : Protocol.CloseRequest + + data class MessageMetaData( + val contentType: String, + val destination: String + ) : Protocol.MessageMetaData + } \ No newline at end of file diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompDestination.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompDestination.kt index f81750b6..f18adff5 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompDestination.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompDestination.kt @@ -19,18 +19,22 @@ class OkHttpStompDestination( override fun createChannelFactory() = SimpleChannelFactory { listener, parent -> require(parent is OkHttpStompMainChannel) - OkHttpStompMessageChannel(parent, destination, listener) + OkHttpStompMessageChannel(destination, parent, parent, listener) } override fun createOpenRequestFactory(channel: Channel) = SimpleProtocolOpenRequestFactory { DestinationOpenRequest(openRequestFactory.createDestinationOpenRequestHeader(destination)) } + override fun createOutgoingMessageMetaDataFactory(channel: Channel): Protocol.MessageMetaData.Factory { + return super.createOutgoingMessageMetaDataFactory(channel) + } + override fun createEventAdapterFactory(): ProtocolSpecificEventAdapter.Factory { return object : ProtocolSpecificEventAdapter.Factory {} } - open class SimpleRequestFactory( + class SimpleRequestFactory( private val createDestinationOpenRequestHeaderCallable: (String) -> Map ) : RequestFactory { @@ -39,10 +43,6 @@ class OkHttpStompDestination( } } - data class MessageMetaData( - val headers: Map - ) : Protocol.MessageMetaData - data class DestinationOpenRequest( val headers: Map ) : Protocol.OpenRequest diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompMainChannel.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompMainChannel.kt index 5ad4a94a..9bf31699 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompMainChannel.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompMainChannel.kt @@ -1,22 +1,32 @@ package com.tinder.scarlet.stomp.okhttp import com.tinder.scarlet.Channel +import com.tinder.scarlet.Message +import com.tinder.scarlet.MessageQueue import com.tinder.scarlet.Protocol +import com.tinder.scarlet.stomp.core.StompCommand +import com.tinder.scarlet.stomp.core.StompListener +import com.tinder.scarlet.stomp.core.StompMessage +import com.tinder.scarlet.stomp.core.StompSubscriber +import com.tinder.scarlet.stomp.support.StompHeaderAccessor import okhttp3.Response import okhttp3.WebSocket import okhttp3.WebSocketListener import okio.ByteString +import java.util.concurrent.ConcurrentHashMap class OkHttpStompMainChannel( private val webSocketFactory: WebSocketFactory, private val listener: Channel.Listener -) : Channel { +) : Channel, MessageQueue, StompSubscriber { + private val topicIds = ConcurrentHashMap() + private val stompListeners = ConcurrentHashMap() private var webSocket: WebSocket? = null override fun open(openRequest: Protocol.OpenRequest) { - val openSocketRequest = openRequest as OkHttpStompClient.ClientOpenSocketRequest - webSocketFactory.createWebSocket(openSocketRequest.okHttpRequest, InnerWebSocketListener()) + val clientOpenRequest = openRequest as OkHttpStompClient.ClientOpenRequest + webSocketFactory.createWebSocket(clientOpenRequest.okHttpRequest, InnerWebSocketListener()) } override fun forceClose() { @@ -27,24 +37,46 @@ class OkHttpStompMainChannel( override fun close(closeRequest: Protocol.CloseRequest) { TODO("Implement send disconnect message") - webSocket?.close(1000, "")//todo add code and reason + + val clientCloseRequest = closeRequest as OkHttpStompClient.ClientCloseRequest + webSocket?.close(clientCloseRequest.code, clientCloseRequest.reason) webSocket = null } - fun sendMessage(destination: String, message: String, headers: Map): Boolean { - TODO("Implement send message to destination") + override fun send(message: Message, messageMetaData: Protocol.MessageMetaData): Boolean { + val metaData = messageMetaData as OkHttpStompClient.MessageMetaData + val messageValue = message as Message.Text + val stompHeader = StompHeaderAccessor.ofHeaders(StompCommand.SEND) + .apply { + destination(metaData.destination) + contentType(metaData.contentType) + } + .createHeader() + val stompMessage = StompMessage(messageValue.value, stompHeader) + return sendStompMessage(stompMessage) } - fun subscribe( + override fun subscribe( destination: String, headers: Map, - listener: (String, Map) -> Unit + listener: StompListener ) { - TODO("Implement subscribe to destination") + val stompHeader = StompHeaderAccessor.ofHeaders(StompCommand.SUBSCRIBE, headers) + .apply { destination(destination) } + .createHeader() + topicIds[destination] = stompHeader.id + val stompMessage = StompMessage("", stompHeader) + sendStompMessage(stompMessage) + } + + override fun unsubscribe(destination: String) { + TODO("send unsubscribe message") + stompListeners.remove(destination) } - fun unSubscribe(destination: String) { - TODO("Implement un subscribe from destination") + private fun sendStompMessage(stompMessage: StompMessage): Boolean { + val text = StompMessageEncoder.encode(stompMessage) + return webSocket?.send(text) ?: false } inner class InnerWebSocketListener : WebSocketListener() { diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompMessageChannel.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompMessageChannel.kt index 8014c7b1..bd9bec26 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompMessageChannel.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompMessageChannel.kt @@ -4,10 +4,12 @@ import com.tinder.scarlet.Channel import com.tinder.scarlet.Message import com.tinder.scarlet.MessageQueue import com.tinder.scarlet.Protocol +import com.tinder.scarlet.stomp.core.StompSubscriber class OkHttpStompMessageChannel( - private val mainChannel: OkHttpStompMainChannel, private val destination: String, + private val stompSubscriber: StompSubscriber, + private val stompSender: MessageQueue, private val listener: Channel.Listener ) : Channel, MessageQueue { @@ -15,15 +17,12 @@ class OkHttpStompMessageChannel( override fun open(openRequest: Protocol.OpenRequest) { val destinationOpenRequest = openRequest as OkHttpStompDestination.DestinationOpenRequest - mainChannel.subscribe( - destination, - destinationOpenRequest.headers - ) { message, headers -> + stompSubscriber.subscribe(destination, destinationOpenRequest.headers) { message, headers -> messageQueueListener?.onMessageReceived( - this, - this, - Message.Text(message), - OkHttpStompDestination.MessageMetaData(headers) + channel = this, + messageQueue = this, + message = message +// metadata = OkHttpStompClient.MessageMetaData(headers) ) } listener.onOpened(this) @@ -34,7 +33,7 @@ class OkHttpStompMessageChannel( } override fun forceClose() { - mainChannel.unSubscribe(destination) + stompSubscriber.unsubscribe(destination) listener.onClosed(this) } @@ -44,16 +43,12 @@ class OkHttpStompMessageChannel( return this } - override fun send(message: Message, messageMetaData: Protocol.MessageMetaData): Boolean { - val headers = messageMetaData as? OkHttpStompDestination.MessageMetaData - return when (message) { - is Message.Text -> mainChannel.sendMessage( - destination, - message.value, - headers?.headers.orEmpty() - ) - is Message.Bytes -> throw IllegalArgumentException("Bytes are not supported") - } + override fun send( + message: Message, + messageMetaData: Protocol.MessageMetaData + ): Boolean = when (message) { + is Message.Text -> stompSender.send(message, messageMetaData) + is Message.Bytes -> throw IllegalArgumentException("Bytes are not supported") } } diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompHeaderAccessor.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompHeaderAccessor.kt new file mode 100644 index 00000000..a4e7b56e --- /dev/null +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompHeaderAccessor.kt @@ -0,0 +1,55 @@ +package com.tinder.scarlet.stomp.support + +import com.tinder.scarlet.stomp.core.StompHeader + +class StompHeaderAccessor private constructor(headers: Map) { + + companion object { + + fun of( + headers: Map = emptyMap() + ): StompHeaderAccessor { + return StompHeaderAccessor(headers) + } + + } + + private val mutableHeaders = mutableMapOf().apply { + putAll(headers) + } + + fun putAll(headers: Map) { + mutableHeaders.putAll(headers) + } + + fun subscriptionId(subscriptionId: String) { + mutableHeaders[StompHeader.ID] = subscriptionId + } + + fun destination(destination: String) { + mutableHeaders[StompHeader.DESTINATION] = destination + } + + fun acceptVersion(acceptVersion: String) { + mutableHeaders[StompHeader.ACCEPT_VERSION] = acceptVersion + } + + fun contentType(contentType: String) { + mutableHeaders[StompHeader.CONTENT_TYPE] = contentType + } + + fun host(host: String) { + mutableHeaders[StompHeader.HOST] = host + } + + fun login(login: String) { + mutableHeaders[StompHeader.LOGIN] = login + } + + fun passcode(passcode: String) { + mutableHeaders[StompHeader.PASSCODE] = passcode + } + + fun createHeader() = StompHeader(mutableHeaders) + +} \ No newline at end of file From a90fe5a83f6406627338c7825aeb1cdbb58caf9d Mon Sep 17 00:00:00 2001 From: NikitaZaltsman Date: Fri, 10 Jan 2020 12:50:25 +0600 Subject: [PATCH 06/32] Add mock implementation for stomp message encoder and message decoder --- .../scarlet/stomp/support/StompMessageDecoder.kt | 15 +++++++++++++++ .../scarlet/stomp/support/StompMessageEncoder.kt | 12 ++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompMessageDecoder.kt create mode 100644 scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompMessageEncoder.kt diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompMessageDecoder.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompMessageDecoder.kt new file mode 100644 index 00000000..3aadb838 --- /dev/null +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompMessageDecoder.kt @@ -0,0 +1,15 @@ +package com.tinder.scarlet.stomp.support + +import com.tinder.scarlet.stomp.core.StompCommand +import com.tinder.scarlet.stomp.core.StompMessage + +object StompMessageDecoder { + + //todo decoder to message + fun decode(text: String): StompMessage { + return StompMessage.Builder() + .withPayload("foo") + .create(StompCommand.MESSAGE) + } + +} \ No newline at end of file diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompMessageEncoder.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompMessageEncoder.kt new file mode 100644 index 00000000..4b7ca6cf --- /dev/null +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompMessageEncoder.kt @@ -0,0 +1,12 @@ +package com.tinder.scarlet.stomp.support + +import com.tinder.scarlet.stomp.core.StompMessage + +object StompMessageEncoder { + + //todo encoder to string + fun encode(stompMessage: StompMessage): String { + return "" + } + +} \ No newline at end of file From cee1d9c9b8be15b5b30be1063123ffa672ce349c Mon Sep 17 00:00:00 2001 From: NikitaZaltsman Date: Fri, 10 Jan 2020 12:55:41 +0600 Subject: [PATCH 07/32] Update okhttpstompdestination, change interfaces to typealias --- .../scarlet/stomp/okhttp/OkHttpStompClient.kt | 5 --- .../stomp/okhttp/OkHttpStompDestination.kt | 34 +++++++++++-------- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompClient.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompClient.kt index f153c6fc..7a45b64e 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompClient.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompClient.kt @@ -72,9 +72,4 @@ class OkHttpStompClient( val reason: String ) : Protocol.CloseRequest - data class MessageMetaData( - val contentType: String, - val destination: String - ) : Protocol.MessageMetaData - } \ No newline at end of file diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompDestination.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompDestination.kt index f18adff5..1bbdf489 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompDestination.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompDestination.kt @@ -1,50 +1,54 @@ package com.tinder.scarlet.stomp.okhttp import com.tinder.scarlet.Channel +import com.tinder.scarlet.Message import com.tinder.scarlet.Protocol import com.tinder.scarlet.ProtocolSpecificEventAdapter +import com.tinder.scarlet.stomp.core.StompHeader import com.tinder.scarlet.utils.SimpleChannelFactory import com.tinder.scarlet.utils.SimpleProtocolOpenRequestFactory +typealias DestinationOpenRequestHeaderFactory = () -> OkHttpStompDestination.DestinationOpenRequest +typealias MessageMetaDataFactory = (channel: Channel, message: Message) -> OkHttpStompDestination.MessageMetaData + class OkHttpStompDestination( private val destination: String, - private val openRequestFactory: RequestFactory + private val openRequestFactory: DestinationOpenRequestHeaderFactory, + private val createMessageMetaDataCallable: MessageMetaDataFactory ) : Protocol { - interface RequestFactory { - - fun createDestinationOpenRequestHeader(destination: String): Map - - } - override fun createChannelFactory() = SimpleChannelFactory { listener, parent -> require(parent is OkHttpStompMainChannel) OkHttpStompMessageChannel(destination, parent, parent, listener) } override fun createOpenRequestFactory(channel: Channel) = SimpleProtocolOpenRequestFactory { - DestinationOpenRequest(openRequestFactory.createDestinationOpenRequestHeader(destination)) + openRequestFactory() } override fun createOutgoingMessageMetaDataFactory(channel: Channel): Protocol.MessageMetaData.Factory { - return super.createOutgoingMessageMetaDataFactory(channel) + return SimpleMessageMetaDataFactory(createMessageMetaDataCallable) } override fun createEventAdapterFactory(): ProtocolSpecificEventAdapter.Factory { return object : ProtocolSpecificEventAdapter.Factory {} } - class SimpleRequestFactory( - private val createDestinationOpenRequestHeaderCallable: (String) -> Map - ) : RequestFactory { + class SimpleMessageMetaDataFactory( + private val createMessageMetaDataCallable: MessageMetaDataFactory + ) : Protocol.MessageMetaData.Factory { - override fun createDestinationOpenRequestHeader(destination: String): Map { - return createDestinationOpenRequestHeaderCallable(destination) + override fun create(channel: Channel, message: Message): Protocol.MessageMetaData { + return createMessageMetaDataCallable(channel, message) } } + data class MessageMetaData( + val headers: StompHeader + ) : Protocol.MessageMetaData + data class DestinationOpenRequest( - val headers: Map + val headers: StompHeader ) : Protocol.OpenRequest } \ No newline at end of file From e0fc49ae2883c36b256b869ca46775c9d09a8ad7 Mon Sep 17 00:00:00 2001 From: NikitaZaltsman Date: Fri, 10 Jan 2020 12:56:06 +0600 Subject: [PATCH 08/32] Add send message methods --- .../stomp/okhttp/OkHttpStompMainChannel.kt | 138 ++++++++++++++---- 1 file changed, 110 insertions(+), 28 deletions(-) diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompMainChannel.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompMainChannel.kt index 9bf31699..5bfdd7b7 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompMainChannel.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompMainChannel.kt @@ -1,77 +1,129 @@ package com.tinder.scarlet.stomp.okhttp import com.tinder.scarlet.Channel -import com.tinder.scarlet.Message -import com.tinder.scarlet.MessageQueue import com.tinder.scarlet.Protocol import com.tinder.scarlet.stomp.core.StompCommand +import com.tinder.scarlet.stomp.core.StompHeader import com.tinder.scarlet.stomp.core.StompListener import com.tinder.scarlet.stomp.core.StompMessage +import com.tinder.scarlet.stomp.core.StompSender import com.tinder.scarlet.stomp.core.StompSubscriber import com.tinder.scarlet.stomp.support.StompHeaderAccessor +import com.tinder.scarlet.stomp.support.StompMessageDecoder +import com.tinder.scarlet.stomp.support.StompMessageEncoder import okhttp3.Response import okhttp3.WebSocket import okhttp3.WebSocketListener import okio.ByteString +import java.util.UUID import java.util.concurrent.ConcurrentHashMap class OkHttpStompMainChannel( private val webSocketFactory: WebSocketFactory, private val listener: Channel.Listener -) : Channel, MessageQueue, StompSubscriber { +) : Channel, StompSender, StompSubscriber { + + companion object { + + private const val ACCEPT_VERSION = "1.1,1.2" + } private val topicIds = ConcurrentHashMap() - private val stompListeners = ConcurrentHashMap() + private val subscriptions = ConcurrentHashMap() private var webSocket: WebSocket? = null override fun open(openRequest: Protocol.OpenRequest) { val clientOpenRequest = openRequest as OkHttpStompClient.ClientOpenRequest - webSocketFactory.createWebSocket(clientOpenRequest.okHttpRequest, InnerWebSocketListener()) + webSocketFactory.createWebSocket( + clientOpenRequest.okHttpRequest, + InnerWebSocketListener(clientOpenRequest) + ) } override fun forceClose() { - TODO("Implement send disconnect message") + disconnectMessage() webSocket?.cancel() webSocket = null } override fun close(closeRequest: Protocol.CloseRequest) { - TODO("Implement send disconnect message") - + disconnectMessage() val clientCloseRequest = closeRequest as OkHttpStompClient.ClientCloseRequest webSocket?.close(clientCloseRequest.code, clientCloseRequest.reason) webSocket = null } - override fun send(message: Message, messageMetaData: Protocol.MessageMetaData): Boolean { - val metaData = messageMetaData as OkHttpStompClient.MessageMetaData - val messageValue = message as Message.Text - val stompHeader = StompHeaderAccessor.ofHeaders(StompCommand.SEND) - .apply { - destination(metaData.destination) - contentType(metaData.contentType) - } + override fun convertAndSend( + payload: String, + destination: String, + headers: StompHeader? + ): Boolean { + val stompHeader = StompHeaderAccessor.of(headers.orEmpty()) + .apply { destination(destination) } .createHeader() - val stompMessage = StompMessage(messageValue.value, stompHeader) + + val stompMessage = StompMessage.Builder() + .withPayload(payload) + .withHeaders(stompHeader) + .create(StompCommand.SEND) + return sendStompMessage(stompMessage) } override fun subscribe( destination: String, - headers: Map, + headers: StompHeader?, listener: StompListener ) { - val stompHeader = StompHeaderAccessor.ofHeaders(StompCommand.SUBSCRIBE, headers) - .apply { destination(destination) } + check(!topicIds.containsKey(destination)) { "Already has subscription to destination=$destination" } + check(!subscriptions.containsKey(destination)) { "Already has subscription to destination=$destination" } + val generateId = UUID.randomUUID().toString() + val stompHeader = StompHeaderAccessor.of(headers.orEmpty()) + .apply { + subscriptionId(generateId) + destination(destination) + } .createHeader() - topicIds[destination] = stompHeader.id - val stompMessage = StompMessage("", stompHeader) + + val stompMessage = StompMessage.Builder() + .withHeaders(stompHeader) + .create(StompCommand.SUBSCRIBE) + sendStompMessage(stompMessage) + + topicIds[destination] = generateId + subscriptions[destination] = listener } override fun unsubscribe(destination: String) { - TODO("send unsubscribe message") - stompListeners.remove(destination) + val subscriptionId = topicIds.remove(destination) + ?: throw IllegalStateException("Unknown destination=$destination") + + val stompHeader = StompHeaderAccessor.of() + .apply { + subscriptionId(subscriptionId) + destination(destination) + } + .createHeader() + + val stompMessage = StompMessage.Builder() + .withHeaders(stompHeader) + .create(StompCommand.UNSUBSCRIBE) + + sendStompMessage(stompMessage) + subscriptions.remove(destination) + } + + private fun handleIncome(stompMessage: StompMessage) = when (stompMessage.command) { + StompCommand.CONNECTED -> listener.onOpened(this) + StompCommand.MESSAGE -> { + val destination = stompMessage.header.destination ?: throw IllegalStateException() + val listener = subscriptions[destination] + listener?.invoke(stompMessage) + } + StompCommand.UNKNOWN -> Unit //heart beat + StompCommand.ERROR -> listener.onFailed(this, true, null) + else -> Unit //not a server message } private fun sendStompMessage(stompMessage: StompMessage): Boolean { @@ -79,18 +131,23 @@ class OkHttpStompMainChannel( return webSocket?.send(text) ?: false } - inner class InnerWebSocketListener : WebSocketListener() { + inner class InnerWebSocketListener( + private val openRequest: OkHttpStompClient.ClientOpenRequest + ) : WebSocketListener() { override fun onOpen(webSocket: WebSocket, response: Response) { this@OkHttpStompMainChannel.webSocket = webSocket - TODO("Send connect message") + val (host, login, passcode) = openRequest + connectMessage(host, login, passcode) } override fun onMessage(webSocket: WebSocket, bytes: ByteString) { - TODO("Implement handle stomp message") + val stompMessage = StompMessageDecoder.decode(bytes.utf8()) + handleIncome(stompMessage) } override fun onMessage(webSocket: WebSocket, text: String) { - TODO("Implement handle stomp message") + val stompMessage = StompMessageDecoder.decode(text) + handleIncome(stompMessage) } override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { @@ -106,6 +163,31 @@ class OkHttpStompMainChannel( listener.onFailed(this@OkHttpStompMainChannel, true, throwable) this@OkHttpStompMainChannel.webSocket = null } + + } + + private fun connectMessage(host: String, login: String? = null, passcode: String? = null) { + val stompHeader = StompHeaderAccessor.of() + .apply { + host(host) + acceptVersion(ACCEPT_VERSION) + login?.let(::login) + passcode?.let(::passcode) + } + .createHeader() + + val stompMessage = StompMessage.Builder() + .withHeaders(stompHeader) + .create(StompCommand.CONNECT) + + sendStompMessage(stompMessage) + } + + private fun disconnectMessage() { + val stompMessage = StompMessage.Builder() + .create(StompCommand.DISCONNECT) + + sendStompMessage(stompMessage) } class Factory( From ba1f936e79ddb1c77a630093b7474f656992fd66 Mon Sep 17 00:00:00 2001 From: NikitaZaltsman Date: Fri, 10 Jan 2020 12:56:48 +0600 Subject: [PATCH 09/32] Add message sender to OkHttpStompMessageChannel Fix code style Add encode and decode implementation Fix naming --- .../tinder/scarlet/stomp/core/StompMessage.kt | 8 +- .../stomp/okhttp/OkHttpStompMainChannel.kt | 13 ++- .../stomp/okhttp/OkHttpStompMessageChannel.kt | 15 ++- .../stomp/support/StompMessageDecoder.kt | 36 +++++- .../stomp/support/StompMessageEncoder.kt | 110 +++++++++++++++++- .../stomp/support/StompMessageEncoderTest.kt | 33 ++++++ 6 files changed, 193 insertions(+), 22 deletions(-) create mode 100644 scarlet-protocol-stomp/src/test/java/com/tinder/scarlet/stomp/support/StompMessageEncoderTest.kt diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompMessage.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompMessage.kt index 9e94c99a..c8d2fdec 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompMessage.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompMessage.kt @@ -5,7 +5,7 @@ import com.tinder.scarlet.stomp.support.StompHeaderAccessor class StompMessage private constructor( val command: StompCommand, val payload: String?, - val header: StompHeader + val headers: StompHeader ) { class Builder { @@ -13,7 +13,7 @@ class StompMessage private constructor( private var payload: String? = null private var headers: StompHeaderAccessor = StompHeaderAccessor.of() - fun withPayload(payload: String): Builder { + fun withPayload(payload: String?): Builder { this.payload = payload return this } @@ -40,7 +40,7 @@ class StompMessage private constructor( if (command != other.command) return false if (payload != other.payload) return false - if (header != other.header) return false + if (headers != other.headers) return false return true } @@ -48,7 +48,7 @@ class StompMessage private constructor( override fun hashCode(): Int { var result = command.hashCode() result = 31 * result + (payload?.hashCode() ?: 0) - result = 31 * result + header.hashCode() + result = 31 * result + headers.hashCode() return result } diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompMainChannel.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompMainChannel.kt index 5bfdd7b7..689862c4 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompMainChannel.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompMainChannel.kt @@ -58,13 +58,13 @@ class OkHttpStompMainChannel( destination: String, headers: StompHeader? ): Boolean { - val stompHeader = StompHeaderAccessor.of(headers.orEmpty()) + val stompHeaders = StompHeaderAccessor.of(headers.orEmpty()) .apply { destination(destination) } .createHeader() val stompMessage = StompMessage.Builder() .withPayload(payload) - .withHeaders(stompHeader) + .withHeaders(stompHeaders) .create(StompCommand.SEND) return sendStompMessage(stompMessage) @@ -78,7 +78,7 @@ class OkHttpStompMainChannel( check(!topicIds.containsKey(destination)) { "Already has subscription to destination=$destination" } check(!subscriptions.containsKey(destination)) { "Already has subscription to destination=$destination" } val generateId = UUID.randomUUID().toString() - val stompHeader = StompHeaderAccessor.of(headers.orEmpty()) + val stompHeaders = StompHeaderAccessor.of(headers.orEmpty()) .apply { subscriptionId(generateId) destination(destination) @@ -86,7 +86,7 @@ class OkHttpStompMainChannel( .createHeader() val stompMessage = StompMessage.Builder() - .withHeaders(stompHeader) + .withHeaders(stompHeaders) .create(StompCommand.SUBSCRIBE) sendStompMessage(stompMessage) @@ -108,6 +108,7 @@ class OkHttpStompMainChannel( val stompMessage = StompMessage.Builder() .withHeaders(stompHeader) + .withHeaders(stompHeaders) .create(StompCommand.UNSUBSCRIBE) sendStompMessage(stompMessage) @@ -117,7 +118,7 @@ class OkHttpStompMainChannel( private fun handleIncome(stompMessage: StompMessage) = when (stompMessage.command) { StompCommand.CONNECTED -> listener.onOpened(this) StompCommand.MESSAGE -> { - val destination = stompMessage.header.destination ?: throw IllegalStateException() + val destination = stompMessage.headers.destination ?: throw IllegalStateException() val listener = subscriptions[destination] listener?.invoke(stompMessage) } @@ -168,6 +169,7 @@ class OkHttpStompMainChannel( private fun connectMessage(host: String, login: String? = null, passcode: String? = null) { val stompHeader = StompHeaderAccessor.of() + val stompHeaders = StompHeaderAccessor.of() .apply { host(host) acceptVersion(ACCEPT_VERSION) @@ -205,7 +207,6 @@ class OkHttpStompMainChannel( } } - } diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompMessageChannel.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompMessageChannel.kt index bd9bec26..0a98d7a3 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompMessageChannel.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompMessageChannel.kt @@ -4,12 +4,13 @@ import com.tinder.scarlet.Channel import com.tinder.scarlet.Message import com.tinder.scarlet.MessageQueue import com.tinder.scarlet.Protocol +import com.tinder.scarlet.stomp.core.StompSender import com.tinder.scarlet.stomp.core.StompSubscriber class OkHttpStompMessageChannel( private val destination: String, private val stompSubscriber: StompSubscriber, - private val stompSender: MessageQueue, + private val stompSender: StompSender, private val listener: Channel.Listener ) : Channel, MessageQueue { @@ -17,12 +18,13 @@ class OkHttpStompMessageChannel( override fun open(openRequest: Protocol.OpenRequest) { val destinationOpenRequest = openRequest as OkHttpStompDestination.DestinationOpenRequest - stompSubscriber.subscribe(destination, destinationOpenRequest.headers) { message, headers -> + val stompHeaders = destinationOpenRequest.headers + stompSubscriber.subscribe(destination, stompHeaders) { message -> messageQueueListener?.onMessageReceived( channel = this, messageQueue = this, - message = message -// metadata = OkHttpStompClient.MessageMetaData(headers) + message = Message.Text(message.payload.orEmpty()), + metadata = OkHttpStompDestination.MessageMetaData(message.headers) ) } listener.onOpened(this) @@ -47,7 +49,10 @@ class OkHttpStompMessageChannel( message: Message, messageMetaData: Protocol.MessageMetaData ): Boolean = when (message) { - is Message.Text -> stompSender.send(message, messageMetaData) + is Message.Text -> { + val metaData = messageMetaData as? OkHttpStompDestination.MessageMetaData + stompSender.convertAndSend(message.value, destination, metaData?.headers) + } is Message.Bytes -> throw IllegalArgumentException("Bytes are not supported") } diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompMessageDecoder.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompMessageDecoder.kt index 3aadb838..08b39450 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompMessageDecoder.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompMessageDecoder.kt @@ -2,14 +2,40 @@ package com.tinder.scarlet.stomp.support import com.tinder.scarlet.stomp.core.StompCommand import com.tinder.scarlet.stomp.core.StompMessage +import com.tinder.scarlet.stomp.support.StompMessageEncoder.TERMINATE_MESSAGE_SYMBOL +import java.io.StringReader +import java.util.Scanner +import java.util.regex.Pattern object StompMessageDecoder { - //todo decoder to message - fun decode(text: String): StompMessage { - return StompMessage.Builder() - .withPayload("foo") - .create(StompCommand.MESSAGE) + private val PATTERN_HEADER = Pattern.compile("([^:\\s]+)\\s*:\\s*([^:\\s]+)") + + fun decode(data: String): StompMessage = if (data.isNotEmpty()) { + val reader = Scanner(StringReader(data)) + reader.useDelimiter("\\n") + + val command = StompCommand.valueOf(reader.next()) + val headerAccessor = StompHeaderAccessor.of() + + while (reader.hasNext(PATTERN_HEADER)) { + val matcher = PATTERN_HEADER.matcher(reader.next()) + if (matcher.find()) { + headerAccessor[matcher.group(1)] = matcher.group(2) + } + } + + reader.skip("\n\n") + reader.useDelimiter(TERMINATE_MESSAGE_SYMBOL.toString()) + val payload = if (reader.hasNext()) reader.next() else null + + StompMessage.Builder() + .withPayload(payload) + .withHeaders(headerAccessor.createHeader()) + .create(command) + } else { + StompMessage.Builder() + .create(StompCommand.UNKNOWN) } } \ No newline at end of file diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompMessageEncoder.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompMessageEncoder.kt index 4b7ca6cf..64841e59 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompMessageEncoder.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompMessageEncoder.kt @@ -1,12 +1,118 @@ package com.tinder.scarlet.stomp.support +import com.tinder.scarlet.stomp.core.StompCommand +import com.tinder.scarlet.stomp.core.StompHeader import com.tinder.scarlet.stomp.core.StompMessage + object StompMessageEncoder { - //todo encoder to string + private const val LF = '\n' + private const val COLON = ':' + + private const val HEARTBEAT_PAYLOAD = "\n" + const val TERMINATE_MESSAGE_SYMBOL = '\u0000' + fun encode(stompMessage: StompMessage): String { - return "" + val stringBuilder = StringBuilder() + + val command = stompMessage.command + if (command == StompCommand.UNKNOWN) { + stringBuilder.append(HEARTBEAT_PAYLOAD) + } else { + val headers = writeHeaders(command, stompMessage.headers, stompMessage.payload.orEmpty()) + stringBuilder + .append(command.toString()) + .append(LF) + .append(headers) + .append(LF) + .apply { stompMessage.payload?.let(::append) } + .append(TERMINATE_MESSAGE_SYMBOL) + } + + return stringBuilder.toString() + } + + private fun writeHeaders( + command: StompCommand, + headers: Map, + payload: String + ): String { + if (headers.isEmpty()) { + return "" + } + val shouldEscape = command != StompCommand.CONNECT && command != StompCommand.CONNECTED + + val stringBuilder = StringBuilder() + headers.forEach { (key, value) -> + if (command.isBodyAllowed && key == StompHeader.CONTENT_LENGTH) { + return@forEach + } + + val headerKey = encode(key, shouldEscape) + val headerValue = encode(value, shouldEscape) + + stringBuilder + .append(headerKey) + .append(COLON) + .append(headerValue) + .append(LF) + } + if (command.isBodyAllowed) { + val contentLength = payload.length + stringBuilder.append(StompHeader.CONTENT_LENGTH) + .append(COLON) + .append(contentLength) + .append(LF) + } + return stringBuilder.toString() + } + + private fun encode(input: String, escape: Boolean): String { + return if (escape) escape(input) else input + } + + /** + * See STOMP Spec 1.2: + * "Value Encoding". + */ + private fun escape(inString: String): String { + var sb: StringBuilder? = null + inString.forEachIndexed { index, symbol -> + when (symbol) { + '\\' -> { + sb = getStringBuilder(sb, inString, index) + sb?.append("\\\\") + } + ':' -> { + sb = getStringBuilder(sb, inString, index) + sb?.append("\\c") + } + '\n' -> { + sb = getStringBuilder(sb, inString, index) + sb?.append("\\n") + } + '\r' -> { + sb = getStringBuilder(sb, inString, index) + sb?.append("\\r") + } + else -> sb?.append(symbol) + } + } + return sb?.toString() ?: inString + } + + private fun getStringBuilder( + stringBuilder: StringBuilder?, + inString: String, + index: Int + ): StringBuilder { + var sb: StringBuilder? = stringBuilder + if (sb == null) { + sb = java.lang.StringBuilder(inString.length) + sb.append(inString.substring(0, index)) + } + return sb } } \ No newline at end of file diff --git a/scarlet-protocol-stomp/src/test/java/com/tinder/scarlet/stomp/support/StompMessageEncoderTest.kt b/scarlet-protocol-stomp/src/test/java/com/tinder/scarlet/stomp/support/StompMessageEncoderTest.kt new file mode 100644 index 00000000..77503ef8 --- /dev/null +++ b/scarlet-protocol-stomp/src/test/java/com/tinder/scarlet/stomp/support/StompMessageEncoderTest.kt @@ -0,0 +1,33 @@ +package com.tinder.scarlet.stomp.support + +import com.tinder.scarlet.stomp.core.StompCommand +import com.tinder.scarlet.stomp.core.StompMessage +import org.junit.Test + +class StompMessageEncoderDecoderTest { + + @Test + fun `correct_decode_message_after_encode`() { + val headers = StompHeaderAccessor.of( + mapOf( + "foo" to "bar", + "bar" to "foo" + ) + ).apply { destination("destination") } + .createHeader() + + val message = StompMessage.Builder() + .withPayload("foo") + .withHeaders(headers) + .create(StompCommand.SEND) + + val data = StompMessageEncoder.encode(message) + val decodeMessage = StompMessageDecoder.decode(data) + + assert(decodeMessage.payload == "foo") + assert(decodeMessage.headers.destination == "destination") + + assert(decodeMessage.headers.size == 4)//because encoder add content-lenth + } + +} \ No newline at end of file From f082436c74946f70c7790b8da1d6b075deb499b5 Mon Sep 17 00:00:00 2001 From: NikitaZaltsman Date: Fri, 10 Jan 2020 18:38:55 +0600 Subject: [PATCH 10/32] add heart beat header --- .../main/java/com/tinder/scarlet/stomp/core/StompHeader.kt | 6 ++++++ .../com/tinder/scarlet/stomp/support/StompHeaderAccessor.kt | 3 +++ 2 files changed, 9 insertions(+) diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompHeader.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompHeader.kt index 90222266..be820c82 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompHeader.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompHeader.kt @@ -52,4 +52,10 @@ class StompHeader( val destination: String? get() = headers[DESTINATION] + val heartBeat: Pair + get() { + val heartBeat = headers[HEARTBEAT] ?: return 0L to 0L + val (sendInterval, receiveInterval) = heartBeat.split(",") + return sendInterval.toLong() to receiveInterval.toLong() + } } \ No newline at end of file diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompHeaderAccessor.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompHeaderAccessor.kt index a4e7b56e..dd1a136e 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompHeaderAccessor.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompHeaderAccessor.kt @@ -18,6 +18,9 @@ class StompHeaderAccessor private constructor(headers: Map) { putAll(headers) } + fun heartBeat(sendInterval: Long, receiveInterval: Long) { + mutableHeaders[StompHeader.HEARTBEAT] = "$sendInterval,$receiveInterval" + } fun putAll(headers: Map) { mutableHeaders.putAll(headers) } From 9d79bdfb314fb57ab7764f7d8779243d7a640686 Mon Sep 17 00:00:00 2001 From: NikitaZaltsman Date: Fri, 10 Jan 2020 18:39:43 +0600 Subject: [PATCH 11/32] Remove extra request and clear subscriptions --- .../stomp/okhttp/OkHttpStompMainChannel.kt | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompMainChannel.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompMainChannel.kt index 689862c4..b4f308b7 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompMainChannel.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompMainChannel.kt @@ -41,16 +41,22 @@ class OkHttpStompMainChannel( } override fun forceClose() { - disconnectMessage() - webSocket?.cancel() - webSocket = null + topicIds.clear() + subscriptions.clear() + + sendDisconnectMessage() + connection?.forceClose() + connection = null } override fun close(closeRequest: Protocol.CloseRequest) { - disconnectMessage() - val clientCloseRequest = closeRequest as OkHttpStompClient.ClientCloseRequest - webSocket?.close(clientCloseRequest.code, clientCloseRequest.reason) - webSocket = null + topicIds.clear() + subscriptions.clear() + + sendDisconnectMessage() + + connection?.close() + connection = null } override fun convertAndSend( From ac7e5cf517f8301aec74c036f76eacfa7cbdbfaf Mon Sep 17 00:00:00 2001 From: NikitaZaltsman Date: Fri, 10 Jan 2020 18:41:14 +0600 Subject: [PATCH 12/32] Add connection abstraction for easy heart beat control --- .../tinder/scarlet/stomp/okhttp/Connection.kt | 37 +++++++++++ .../stomp/okhttp/WebSocketConnection.kt | 65 +++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/Connection.kt create mode 100644 scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/WebSocketConnection.kt diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/Connection.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/Connection.kt new file mode 100644 index 00000000..dfc2df1e --- /dev/null +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/Connection.kt @@ -0,0 +1,37 @@ +package com.tinder.scarlet.stomp.okhttp + +interface Connection { + + /** + * Send the given message. + * @param message the message + * @return a + * message was successfully sent + */ + fun send(message: String): Boolean + + /** + * Register a task to invoke after a period of read inactivity. + * @param runnable the task to invoke + * @param duration the amount of inactive time in milliseconds + */ + fun onReadInactivity(duration: Long, runnable: () -> Unit) + + /** + * Register a task to invoke after a period of write inactivity. + * @param runnable the task to invoke + * @param duration the amount of inactive time in milliseconds + */ + fun onWriteInactivity(duration: Long, runnable: () -> Unit) + + /** + * Force close the connection. + */ + fun forceClose() + + /** + * Close the connection. + */ + fun close() + +} \ No newline at end of file diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/WebSocketConnection.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/WebSocketConnection.kt new file mode 100644 index 00000000..aa97df07 --- /dev/null +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/WebSocketConnection.kt @@ -0,0 +1,65 @@ +package com.tinder.scarlet.stomp.okhttp + +import okhttp3.WebSocket +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit + + +class WebSocketConnection( + private val webSocket: WebSocket +) : Connection { + + @Volatile + private var lastReadTime: Long = -1 + + @Volatile + private var lastWriteTime: Long = -1 + + private val executor = Executors.newSingleThreadScheduledExecutor() + + companion object { + + private const val NORMAL_CLOSURE_STATUS_CODE = 1000 + private const val NORMAL_CLOSURE_REASON = "Normal closure" + + } + + override fun send(message: String): Boolean { + updateLastWriteTime() + return webSocket.send(message) + } + + override fun onReadInactivity(duration: Long, runnable: () -> Unit) { + lastReadTime = System.currentTimeMillis() + executor.scheduleWithFixedDelay({ + if (System.currentTimeMillis() - lastReadTime > duration) { + runnable.invoke() + } + }, 0, duration / 2, TimeUnit.MILLISECONDS) + } + + override fun onWriteInactivity(duration: Long, runnable: () -> Unit) { + lastWriteTime = System.currentTimeMillis() + executor.scheduleWithFixedDelay({ + if (System.currentTimeMillis() - lastWriteTime > duration) { + runnable.invoke() + } + }, 0, duration / 2, TimeUnit.MILLISECONDS) + } + + override fun forceClose() { + webSocket.cancel() + } + + override fun close() { + webSocket.close(NORMAL_CLOSURE_STATUS_CODE, NORMAL_CLOSURE_REASON) + } + + private fun updateLastWriteTime() { + val lastWriteTime = lastWriteTime + if (lastWriteTime != -1L) { + this.lastWriteTime = System.currentTimeMillis() + } + } + +} \ No newline at end of file From f5c19ab56d080bacce34ca56425d9e85e7ff1dcb Mon Sep 17 00:00:00 2001 From: NikitaZaltsman Date: Fri, 10 Jan 2020 18:41:51 +0600 Subject: [PATCH 13/32] Add heart beat field to ClientOpenRequest and remove ClientCloseRequest --- .../scarlet/stomp/okhttp/OkHttpStompClient.kt | 25 +++---------------- 1 file changed, 4 insertions(+), 21 deletions(-) diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompClient.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompClient.kt index 7a45b64e..41f55268 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompClient.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompClient.kt @@ -3,8 +3,6 @@ package com.tinder.scarlet.stomp.okhttp import com.tinder.scarlet.Channel import com.tinder.scarlet.Protocol import com.tinder.scarlet.ProtocolSpecificEventAdapter -import com.tinder.scarlet.stomp.core.StompHeader -import com.tinder.scarlet.utils.SimpleProtocolCloseRequestFactory import com.tinder.scarlet.utils.SimpleProtocolOpenRequestFactory import okhttp3.OkHttpClient import okhttp3.Request @@ -31,30 +29,18 @@ class OkHttpStompClient( } } - override fun createCloseRequestFactory(channel: Channel): Protocol.CloseRequest.Factory { - return SimpleProtocolCloseRequestFactory { - requestFactory.createClientCloseRequest() - } - } - override fun createEventAdapterFactory(): ProtocolSpecificEventAdapter.Factory { return object : ProtocolSpecificEventAdapter.Factory {} } interface RequestFactory { - fun createClientCloseRequest(): ClientCloseRequest fun createClientOpenRequest(): ClientOpenRequest } open class SimpleRequestFactory( - private val createClientCloseRequestCallable: () -> ClientCloseRequest, private val createClientOpenRequestCallable: () -> ClientOpenRequest ) : RequestFactory { - override fun createClientCloseRequest(): ClientCloseRequest { - return createClientCloseRequestCallable() - } - override fun createClientOpenRequest(): ClientOpenRequest { return createClientOpenRequestCallable() } @@ -62,14 +48,11 @@ class OkHttpStompClient( data class ClientOpenRequest( val host: String, + val okHttpRequest: Request, + val heartbeatSendInterval: Long = 0, + val heartbeatReceiveInterval: Long = 0, val login: String? = null, - val passcode: String? = null, - val okHttpRequest: Request + val passcode: String? = null ) : Protocol.OpenRequest - data class ClientCloseRequest( - val code: Int, - val reason: String - ) : Protocol.CloseRequest - } \ No newline at end of file From ce2306720b88cea1603ba07fca42186bfcebbeb0 Mon Sep 17 00:00:00 2001 From: NikitaZaltsman Date: Fri, 10 Jan 2020 18:42:07 +0600 Subject: [PATCH 14/32] Remove gazirra stomp client --- scarlet-protocol-stomp/build.gradle | 1 - .../scarlet/stomp/GozirraStompClient.kt | 195 ------------------ 2 files changed, 196 deletions(-) delete mode 100644 scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/GozirraStompClient.kt diff --git a/scarlet-protocol-stomp/build.gradle b/scarlet-protocol-stomp/build.gradle index 239d8406..88a730b0 100644 --- a/scarlet-protocol-stomp/build.gradle +++ b/scarlet-protocol-stomp/build.gradle @@ -8,7 +8,6 @@ apply plugin: 'java-library' dependencies { api project(':scarlet-core') - implementation files('libs/gozirra-client-0.4.1.jar') implementation project(':scarlet-core-internal') implementation rootProject.ext.rxJava diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/GozirraStompClient.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/GozirraStompClient.kt deleted file mode 100644 index f09961dd..00000000 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/GozirraStompClient.kt +++ /dev/null @@ -1,195 +0,0 @@ -/* - * © 2018 Match Group, LLC. - */ - -package com.tinder.scarlet.stomp - -import com.tinder.scarlet.Channel -import com.tinder.scarlet.Message -import com.tinder.scarlet.MessageQueue -import com.tinder.scarlet.Protocol -import com.tinder.scarlet.ProtocolSpecificEventAdapter -import com.tinder.scarlet.utils.SimpleChannelFactory -import com.tinder.scarlet.utils.SimpleProtocolOpenRequestFactory -import net.ser1.stomp.Client -import javax.security.auth.login.LoginException - -class GozirraStompClient( - private val openRequestFactory: RequestFactory -) : Protocol { - - override fun createChannelFactory(): Channel.Factory { - return SimpleChannelFactory { listener, _ -> - StompMainChannel(listener) - } - } - - override fun createOpenRequestFactory(channel: Channel): Protocol.OpenRequest.Factory { - return SimpleProtocolOpenRequestFactory { - openRequestFactory.createClientOpenRequest() - } - } - - override fun createEventAdapterFactory(): ProtocolSpecificEventAdapter.Factory { - return object : ProtocolSpecificEventAdapter.Factory {} - } - - interface RequestFactory { - fun createClientOpenRequest(): ClientOpenRequest - } - - open class SimpleRequestFactory( - private val createClientOpenRequestCallable: () -> ClientOpenRequest - ) : RequestFactory { - override fun createClientOpenRequest(): ClientOpenRequest { - return createClientOpenRequestCallable() - } - } - - data class ClientOpenRequest( - val url: String, - val port: Int, - val login: String, - val password: String - ) : Protocol.OpenRequest - - data class MessageMetaData(val headers: Map) : Protocol.MessageMetaData -} - -class GozirraStompDestination( - val destination: String, - private val openRequestFactory: RequestFactory -) : Protocol { - - override fun createChannelFactory(): Channel.Factory { - return SimpleChannelFactory { listener, parent -> - require(parent is StompMainChannel) - StompMessageChannel(parent as StompMainChannel, destination, listener) - } - } - - override fun createOpenRequestFactory(channel: Channel): Protocol.OpenRequest.Factory { - return SimpleProtocolOpenRequestFactory { - DestinationOpenRequest( - openRequestFactory.createDestinationOpenRequestHeader(destination) - ) - } - } - - override fun createEventAdapterFactory(): ProtocolSpecificEventAdapter.Factory { - return object : ProtocolSpecificEventAdapter.Factory {} - } - - interface RequestFactory { - fun createDestinationOpenRequestHeader(destination: String): Map - } - - open class SimpleRequestFactory( - private val createDestinationOpenRequestHeaderCallable: (String) -> Map - ) : RequestFactory { - - override fun createDestinationOpenRequestHeader(destination: String): Map { - return createDestinationOpenRequestHeaderCallable(destination) - } - } - - data class DestinationOpenRequest( - val headers: Map - ) : Protocol.OpenRequest -} - -class StompMainChannel( - private val listener: Channel.Listener -) : Channel { - var client: Client? = null - - override fun open(openRequest: Protocol.OpenRequest) { - val (url, port, login, password) = openRequest as GozirraStompClient.ClientOpenRequest - try { - val client = Client(url, port, login, password) - client.addErrorListener { _, _ -> - listener.onFailed(this, true, null) - } - this.client = client - listener.onOpened(this) - } catch (e: LoginException) { - listener.onFailed(this, true, e) - } catch (e: Throwable) { - listener.onFailed(this, true, e) - } - } - - override fun close(closeRequest: Protocol.CloseRequest) { - forceClose() - } - - override fun forceClose() { - try { - client?.disconnect() - client = null - listener.onClosed(this) - } catch (e: LoginException) { - listener.onFailed(this, true, e) - } catch (e: Throwable) { - listener.onFailed(this, true, e) - } - } - - override fun createMessageQueue(listener: MessageQueue.Listener): MessageQueue? { - return null - } -} - -class StompMessageChannel( - private val mainChannel: StompMainChannel, - private val destination: String, - private val listener: Channel.Listener -) : Channel, MessageQueue { - - private var client: Client? = null - private var messageQueueListener: MessageQueue.Listener? = null - - override fun open(openRequest: Protocol.OpenRequest) { - val destinationOpenRequest = openRequest as GozirraStompDestination.DestinationOpenRequest - client = mainChannel.client - client?.subscribe( - destination, - { headers, message -> - messageQueueListener?.onMessageReceived( - this, - this, - Message.Text(message), - GozirraStompClient.MessageMetaData(headers as Map) - ) - }, - // This map is modified by gozirra internally - destinationOpenRequest.headers.toMutableMap() - ) - listener.onOpened(this) - } - - override fun close(closeRequest: Protocol.CloseRequest) { - forceClose() - } - - override fun forceClose() { - client?.unsubscribe(destination) - client = null - listener.onClosed(this) - } - - override fun createMessageQueue(listener: MessageQueue.Listener): MessageQueue { - require(messageQueueListener == null) - messageQueueListener = listener - return this - } - - override fun send(message: Message, messageMetaData: Protocol.MessageMetaData): Boolean { - val client = client ?: return false - when (message) { - is Message.Text -> client.send(destination, message.value) - is Message.Bytes -> throw IllegalArgumentException("Bytes are not supported") - } - return true - } -} From cd25f3f2dd7c48f00b4d3bd584e87c3630cd3eb9 Mon Sep 17 00:00:00 2001 From: NikitaZaltsman Date: Fri, 10 Jan 2020 18:42:49 +0600 Subject: [PATCH 15/32] Add heartbeat support to main channel and add error message custom header Fix bug with update read time Fix demo Code style update --- demo/build.gradle | 4 + .../com/tinder/app/root/view/DemoActivity.kt | 9 +- .../tinder/scarlet/stomp/okhttp/Connection.kt | 7 +- .../scarlet/stomp/okhttp/MessageHandler.kt | 12 ++ .../scarlet/stomp/okhttp/OkHttpStompClient.kt | 39 ++---- .../stomp/okhttp/OkHttpStompDestination.kt | 23 ++-- .../stomp/okhttp/OkHttpStompMainChannel.kt | 115 +++++++++++++----- .../stomp/okhttp/WebSocketConnection.kt | 25 ++-- .../scarlet/stomp/okhttp/WebSocketFactory.kt | 1 - .../stomp/support/StompHeaderAccessor.kt | 11 ++ 10 files changed, 163 insertions(+), 83 deletions(-) create mode 100644 scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/MessageHandler.kt diff --git a/demo/build.gradle b/demo/build.gradle index bd5ea465..6ddcad62 100755 --- a/demo/build.gradle +++ b/demo/build.gradle @@ -23,6 +23,10 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } packagingOptions { exclude 'META-INF/rxjava.properties' exclude 'META-INF/ASL-2.0.txt' diff --git a/demo/src/main/java/com/tinder/app/root/view/DemoActivity.kt b/demo/src/main/java/com/tinder/app/root/view/DemoActivity.kt index 6d231fa5..fd54f9d0 100755 --- a/demo/src/main/java/com/tinder/app/root/view/DemoActivity.kt +++ b/demo/src/main/java/com/tinder/app/root/view/DemoActivity.kt @@ -41,9 +41,12 @@ class DemoActivity : AppCompatActivity() { tabLayout.addOnTabSelectedListener(TabLayout.ViewPagerOnTabSelectedListener(viewPager)) } - private class ViewPagerAdapter constructor(fm: FragmentManager, private val pageCount: Int) : - FragmentStatePagerAdapter(fm) { - override fun getItem(position: Int): Fragment? { + private class ViewPagerAdapter( + fm: FragmentManager, + private val pageCount: Int + ) : FragmentStatePagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { + + override fun getItem(position: Int): Fragment { val (_, createFragment) = TAB_ITEMS[position] return createFragment() } diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/Connection.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/Connection.kt index dfc2df1e..69bea05e 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/Connection.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/Connection.kt @@ -1,14 +1,15 @@ package com.tinder.scarlet.stomp.okhttp +import com.tinder.scarlet.stomp.core.StompMessage + interface Connection { /** * Send the given message. * @param message the message - * @return a - * message was successfully sent + * @return true if the message was enqueued. */ - fun send(message: String): Boolean + fun send(message: StompMessage): Boolean /** * Register a task to invoke after a period of read inactivity. diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/MessageHandler.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/MessageHandler.kt new file mode 100644 index 00000000..c258315c --- /dev/null +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/MessageHandler.kt @@ -0,0 +1,12 @@ +package com.tinder.scarlet.stomp.okhttp + +import com.tinder.scarlet.stomp.core.StompMessage + +interface MessageHandler { + + /** + * Convert given raw data string to stomp message + */ + fun handle(data: String): StompMessage + +} \ No newline at end of file diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompClient.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompClient.kt index 41f55268..07d5d6c5 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompClient.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompClient.kt @@ -8,44 +8,31 @@ import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.WebSocketListener +typealias ClientOpenRequestHeaderFactory = (Channel) -> OkHttpStompClient.ClientOpenRequest + class OkHttpStompClient( private val okHttpClient: OkHttpClient, - private val requestFactory: RequestFactory + private val requestFactory: ClientOpenRequestHeaderFactory ) : Protocol { - override fun createChannelFactory(): Channel.Factory { - return OkHttpStompMainChannel.Factory( - object : WebSocketFactory { - override fun createWebSocket(request: Request, listener: WebSocketListener) { - okHttpClient.newWebSocket(request, listener) - } + override fun createChannelFactory() = OkHttpStompMainChannel.Factory( + object : WebSocketFactory { + override fun createWebSocket(request: Request, listener: WebSocketListener) { + okHttpClient.newWebSocket(request, listener) } - ) - } - - override fun createOpenRequestFactory(channel: Channel): Protocol.OpenRequest.Factory { - return SimpleProtocolOpenRequestFactory { - requestFactory.createClientOpenRequest() } + ) + + override fun createOpenRequestFactory( + channel: Channel + ) = SimpleProtocolOpenRequestFactory { + requestFactory.invoke(channel) } override fun createEventAdapterFactory(): ProtocolSpecificEventAdapter.Factory { return object : ProtocolSpecificEventAdapter.Factory {} } - interface RequestFactory { - fun createClientOpenRequest(): ClientOpenRequest - } - - open class SimpleRequestFactory( - private val createClientOpenRequestCallable: () -> ClientOpenRequest - ) : RequestFactory { - - override fun createClientOpenRequest(): ClientOpenRequest { - return createClientOpenRequestCallable() - } - } - data class ClientOpenRequest( val host: String, val okHttpRequest: Request, diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompDestination.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompDestination.kt index 1bbdf489..49561a54 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompDestination.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompDestination.kt @@ -13,8 +13,8 @@ typealias MessageMetaDataFactory = (channel: Channel, message: Message) -> OkHtt class OkHttpStompDestination( private val destination: String, - private val openRequestFactory: DestinationOpenRequestHeaderFactory, - private val createMessageMetaDataCallable: MessageMetaDataFactory + private val openRequestFactory: DestinationOpenRequestHeaderFactory? = null, + private val createMessageMetaDataCallable: MessageMetaDataFactory? = null ) : Protocol { override fun createChannelFactory() = SimpleChannelFactory { listener, parent -> @@ -23,24 +23,27 @@ class OkHttpStompDestination( } override fun createOpenRequestFactory(channel: Channel) = SimpleProtocolOpenRequestFactory { - openRequestFactory() + openRequestFactory?.invoke() ?: Protocol.OpenRequest.Empty } - override fun createOutgoingMessageMetaDataFactory(channel: Channel): Protocol.MessageMetaData.Factory { - return SimpleMessageMetaDataFactory(createMessageMetaDataCallable) - } + override fun createOutgoingMessageMetaDataFactory( + channel: Channel + ) = SimpleMessageMetaDataFactory(createMessageMetaDataCallable) override fun createEventAdapterFactory(): ProtocolSpecificEventAdapter.Factory { return object : ProtocolSpecificEventAdapter.Factory {} } class SimpleMessageMetaDataFactory( - private val createMessageMetaDataCallable: MessageMetaDataFactory + private val createMessageMetaDataCallable: MessageMetaDataFactory? ) : Protocol.MessageMetaData.Factory { - override fun create(channel: Channel, message: Message): Protocol.MessageMetaData { - return createMessageMetaDataCallable(channel, message) - } + override fun create( + channel: Channel, + message: Message + ) = createMessageMetaDataCallable?.invoke(channel, message) + ?: Protocol.MessageMetaData.Empty + } data class MessageMetaData( diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompMainChannel.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompMainChannel.kt index b4f308b7..f1c9e7f1 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompMainChannel.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompMainChannel.kt @@ -17,6 +17,7 @@ import okhttp3.WebSocketListener import okio.ByteString import java.util.UUID import java.util.concurrent.ConcurrentHashMap +import kotlin.math.max class OkHttpStompMainChannel( private val webSocketFactory: WebSocketFactory, @@ -25,12 +26,20 @@ class OkHttpStompMainChannel( companion object { + /** STOMP recommended error of margin for receiving heartbeats. */ + private const val HEARTBEAT_MULTIPLIER = 3 + private const val ACCEPT_VERSION = "1.1,1.2" } private val topicIds = ConcurrentHashMap() private val subscriptions = ConcurrentHashMap() - private var webSocket: WebSocket? = null + + private var messageHandler: MessageHandler? = null + private var connection: Connection? = null + + private var clientSendInterval: Long = 0 + private var clientReceiveInterval: Long = 0 override fun open(openRequest: Protocol.OpenRequest) { val clientOpenRequest = openRequest as OkHttpStompClient.ClientOpenRequest @@ -46,7 +55,9 @@ class OkHttpStompMainChannel( sendDisconnectMessage() connection?.forceClose() + connection = null + messageHandler = null } override fun close(closeRequest: Protocol.CloseRequest) { @@ -56,7 +67,9 @@ class OkHttpStompMainChannel( sendDisconnectMessage() connection?.close() + connection = null + messageHandler = null } override fun convertAndSend( @@ -73,7 +86,7 @@ class OkHttpStompMainChannel( .withHeaders(stompHeaders) .create(StompCommand.SEND) - return sendStompMessage(stompMessage) + return connection?.send(stompMessage) ?: false } override fun subscribe( @@ -95,7 +108,7 @@ class OkHttpStompMainChannel( .withHeaders(stompHeaders) .create(StompCommand.SUBSCRIBE) - sendStompMessage(stompMessage) + connection?.send(stompMessage) topicIds[destination] = generateId subscriptions[destination] = listener @@ -105,56 +118,92 @@ class OkHttpStompMainChannel( val subscriptionId = topicIds.remove(destination) ?: throw IllegalStateException("Unknown destination=$destination") - val stompHeader = StompHeaderAccessor.of() - .apply { - subscriptionId(subscriptionId) - destination(destination) - } + val stompHeaders = StompHeaderAccessor.of() + .apply { subscriptionId(subscriptionId) } .createHeader() val stompMessage = StompMessage.Builder() - .withHeaders(stompHeader) .withHeaders(stompHeaders) .create(StompCommand.UNSUBSCRIBE) - sendStompMessage(stompMessage) + connection?.send(stompMessage) subscriptions.remove(destination) } private fun handleIncome(stompMessage: StompMessage) = when (stompMessage.command) { - StompCommand.CONNECTED -> listener.onOpened(this) + StompCommand.CONNECTED -> { + setupHeartBeat(stompMessage) + listener.onOpened(this) + } StompCommand.MESSAGE -> { val destination = stompMessage.headers.destination ?: throw IllegalStateException() val listener = subscriptions[destination] listener?.invoke(stompMessage) } - StompCommand.UNKNOWN -> Unit //heart beat StompCommand.ERROR -> listener.onFailed(this, true, null) else -> Unit //not a server message } - private fun sendStompMessage(stompMessage: StompMessage): Boolean { - val text = StompMessageEncoder.encode(stompMessage) - return webSocket?.send(text) ?: false + private fun setupHeartBeat(stompMessage: StompMessage) { + val (serverSendInterval, serverReceiveInterval) = stompMessage.headers.heartBeat + + if (clientSendInterval > 0 && serverReceiveInterval > 0) { + val interval = max(clientSendInterval, serverReceiveInterval) + connection?.onWriteInactivity(interval) { sendHeartBeat() } + } + + if (clientReceiveInterval > 0 && serverSendInterval > 0) { + val interval = max(clientReceiveInterval, serverSendInterval) * HEARTBEAT_MULTIPLIER; + connection?.onReadInactivity(interval) { + sendErrorMessage("No messages received in $interval ms.") + connection?.close() + listener.onFailed(this@OkHttpStompMainChannel, true, null) + } + } + + } + + private fun sendHeartBeat() { + val stompMessage = StompMessage.Builder() + .create(StompCommand.UNKNOWN) + + connection?.send(stompMessage) + } + + private fun sendErrorMessage(error: String) { + val headers = StompHeaderAccessor.of() + .apply { message(error) } + .createHeader() + + val stompMessage = StompMessage.Builder() + .withHeaders(headers) + .create(StompCommand.ERROR) + + connection?.send(stompMessage) } inner class InnerWebSocketListener( private val openRequest: OkHttpStompClient.ClientOpenRequest ) : WebSocketListener() { override fun onOpen(webSocket: WebSocket, response: Response) { - this@OkHttpStompMainChannel.webSocket = webSocket - val (host, login, passcode) = openRequest - connectMessage(host, login, passcode) + val webSocketConnection = WebSocketConnection(webSocket) + + this@OkHttpStompMainChannel.connection = webSocketConnection + this@OkHttpStompMainChannel.messageHandler = webSocketConnection + + val host = openRequest.host + val login = openRequest.login + val passcode = openRequest.passcode + + sendConnectMessage(host, login, passcode) } override fun onMessage(webSocket: WebSocket, bytes: ByteString) { - val stompMessage = StompMessageDecoder.decode(bytes.utf8()) - handleIncome(stompMessage) + messageHandler?.handle(bytes.utf8())?.let(::handleIncome) } override fun onMessage(webSocket: WebSocket, text: String) { - val stompMessage = StompMessageDecoder.decode(text) - handleIncome(stompMessage) + messageHandler?.handle(text)?.let(::handleIncome) } override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { @@ -163,39 +212,43 @@ class OkHttpStompMainChannel( override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { listener.onClosed(this@OkHttpStompMainChannel) - this@OkHttpStompMainChannel.webSocket = null + this@OkHttpStompMainChannel.connection = null } override fun onFailure(webSocket: WebSocket, throwable: Throwable, response: Response?) { listener.onFailed(this@OkHttpStompMainChannel, true, throwable) - this@OkHttpStompMainChannel.webSocket = null + this@OkHttpStompMainChannel.connection = null } } - private fun connectMessage(host: String, login: String? = null, passcode: String? = null) { - val stompHeader = StompHeaderAccessor.of() + private fun sendConnectMessage(host: String, login: String? = null, passcode: String? = null) { val stompHeaders = StompHeaderAccessor.of() .apply { host(host) acceptVersion(ACCEPT_VERSION) login?.let(::login) passcode?.let(::passcode) + + if (clientSendInterval > 0 && clientReceiveInterval > 0) { + heartBeat(clientSendInterval, clientReceiveInterval) + } + } .createHeader() val stompMessage = StompMessage.Builder() - .withHeaders(stompHeader) + .withHeaders(stompHeaders) .create(StompCommand.CONNECT) - sendStompMessage(stompMessage) + connection?.send(stompMessage) } - private fun disconnectMessage() { + private fun sendDisconnectMessage() { val stompMessage = StompMessage.Builder() .create(StompCommand.DISCONNECT) - sendStompMessage(stompMessage) + connection?.send(stompMessage) } class Factory( @@ -214,5 +267,3 @@ class OkHttpStompMainChannel( } } - - diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/WebSocketConnection.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/WebSocketConnection.kt index aa97df07..1dbd06e6 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/WebSocketConnection.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/WebSocketConnection.kt @@ -1,5 +1,8 @@ package com.tinder.scarlet.stomp.okhttp +import com.tinder.scarlet.stomp.core.StompMessage +import com.tinder.scarlet.stomp.support.StompMessageDecoder +import com.tinder.scarlet.stomp.support.StompMessageEncoder import okhttp3.WebSocket import java.util.concurrent.Executors import java.util.concurrent.TimeUnit @@ -7,7 +10,7 @@ import java.util.concurrent.TimeUnit class WebSocketConnection( private val webSocket: WebSocket -) : Connection { +) : Connection, MessageHandler { @Volatile private var lastReadTime: Long = -1 @@ -24,9 +27,13 @@ class WebSocketConnection( } - override fun send(message: String): Boolean { - updateLastWriteTime() - return webSocket.send(message) + override fun send(message: StompMessage): Boolean { + val lastWriteTime = lastWriteTime + if (lastWriteTime != -1L) { + this.lastWriteTime = System.currentTimeMillis() + } + val encodedMessage = StompMessageEncoder.encode(message) + return webSocket.send(encodedMessage) } override fun onReadInactivity(duration: Long, runnable: () -> Unit) { @@ -55,11 +62,13 @@ class WebSocketConnection( webSocket.close(NORMAL_CLOSURE_STATUS_CODE, NORMAL_CLOSURE_REASON) } - private fun updateLastWriteTime() { - val lastWriteTime = lastWriteTime - if (lastWriteTime != -1L) { - this.lastWriteTime = System.currentTimeMillis() + override fun handle(data: String): StompMessage { + val lastReadTime = lastReadTime + if (lastReadTime != -1L) { + this.lastReadTime = System.currentTimeMillis() } + return StompMessageDecoder.decode(data) } + } \ No newline at end of file diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/WebSocketFactory.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/WebSocketFactory.kt index cea3569c..4c4a776c 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/WebSocketFactory.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/WebSocketFactory.kt @@ -1,7 +1,6 @@ /* * © 2018 Match Group, LLC. */ - package com.tinder.scarlet.stomp.okhttp import okhttp3.Request diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompHeaderAccessor.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompHeaderAccessor.kt index dd1a136e..cc0502b2 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompHeaderAccessor.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompHeaderAccessor.kt @@ -6,6 +6,8 @@ class StompHeaderAccessor private constructor(headers: Map) { companion object { + private const val STOMP_MESSAGE_HEADER = "message" + fun of( headers: Map = emptyMap() ): StompHeaderAccessor { @@ -18,9 +20,14 @@ class StompHeaderAccessor private constructor(headers: Map) { putAll(headers) } + operator fun set(key: String, value: String) { + mutableHeaders[key] = value + } + fun heartBeat(sendInterval: Long, receiveInterval: Long) { mutableHeaders[StompHeader.HEARTBEAT] = "$sendInterval,$receiveInterval" } + fun putAll(headers: Map) { mutableHeaders.putAll(headers) } @@ -53,6 +60,10 @@ class StompHeaderAccessor private constructor(headers: Map) { mutableHeaders[StompHeader.PASSCODE] = passcode } + fun message(message: String) { + mutableHeaders[STOMP_MESSAGE_HEADER] = message + } + fun createHeader() = StompHeader(mutableHeaders) } \ No newline at end of file From 3d84e997f3521294cc68e19ef1137187de6b0174 Mon Sep 17 00:00:00 2001 From: NikitaZaltsman Date: Tue, 14 Jan 2020 00:16:28 +0600 Subject: [PATCH 16/32] Add shutdown executor after close connection --- .../java/com/tinder/scarlet/stomp/okhttp/WebSocketConnection.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/WebSocketConnection.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/WebSocketConnection.kt index 1dbd06e6..ef1b4be9 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/WebSocketConnection.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/WebSocketConnection.kt @@ -56,10 +56,12 @@ class WebSocketConnection( override fun forceClose() { webSocket.cancel() + executor.shutdown() } override fun close() { webSocket.close(NORMAL_CLOSURE_STATUS_CODE, NORMAL_CLOSURE_REASON) + executor.shutdown() } override fun handle(data: String): StompMessage { From 3aa1cac31a12350e3c64e827f8c0ec2450b74cb9 Mon Sep 17 00:00:00 2001 From: NikitaZaltsman Date: Tue, 14 Jan 2020 00:18:27 +0600 Subject: [PATCH 17/32] Fix case with message with empty destination and fix bug with setup heart beat settings --- .../stomp/okhttp/OkHttpStompMainChannel.kt | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompMainChannel.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompMainChannel.kt index f1c9e7f1..bc128948 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompMainChannel.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompMainChannel.kt @@ -9,8 +9,6 @@ import com.tinder.scarlet.stomp.core.StompMessage import com.tinder.scarlet.stomp.core.StompSender import com.tinder.scarlet.stomp.core.StompSubscriber import com.tinder.scarlet.stomp.support.StompHeaderAccessor -import com.tinder.scarlet.stomp.support.StompMessageDecoder -import com.tinder.scarlet.stomp.support.StompMessageEncoder import okhttp3.Response import okhttp3.WebSocket import okhttp3.WebSocketListener @@ -43,6 +41,10 @@ class OkHttpStompMainChannel( override fun open(openRequest: Protocol.OpenRequest) { val clientOpenRequest = openRequest as OkHttpStompClient.ClientOpenRequest + + this.clientSendInterval = clientOpenRequest.heartbeatSendInterval + this.clientReceiveInterval = clientOpenRequest.heartbeatReceiveInterval + webSocketFactory.createWebSocket( clientOpenRequest.okHttpRequest, InnerWebSocketListener(clientOpenRequest) @@ -135,11 +137,11 @@ class OkHttpStompMainChannel( setupHeartBeat(stompMessage) listener.onOpened(this) } - StompCommand.MESSAGE -> { - val destination = stompMessage.headers.destination ?: throw IllegalStateException() - val listener = subscriptions[destination] - listener?.invoke(stompMessage) - } + StompCommand.MESSAGE -> stompMessage.headers.destination + ?.let { destination -> + val listener = subscriptions[destination] + listener?.invoke(stompMessage) + } StompCommand.ERROR -> listener.onFailed(this, true, null) else -> Unit //not a server message } From 9cb297539a3d0b6c60a1be2f460b054f4581784e Mon Sep 17 00:00:00 2001 From: NikitaZaltsman Date: Tue, 14 Jan 2020 00:43:57 +0600 Subject: [PATCH 18/32] Rename Message Channel --- ...ttpStompDestination.kt => StompDestination.kt} | 11 +++++++---- ...mpMessageChannel.kt => StompMessageChannel.kt} | 15 +++++++++------ 2 files changed, 16 insertions(+), 10 deletions(-) rename scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/{OkHttpStompDestination.kt => StompDestination.kt} (87%) rename scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/{OkHttpStompMessageChannel.kt => StompMessageChannel.kt} (74%) diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompDestination.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/StompDestination.kt similarity index 87% rename from scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompDestination.kt rename to scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/StompDestination.kt index 49561a54..cf08d01e 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompDestination.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/StompDestination.kt @@ -8,10 +8,13 @@ import com.tinder.scarlet.stomp.core.StompHeader import com.tinder.scarlet.utils.SimpleChannelFactory import com.tinder.scarlet.utils.SimpleProtocolOpenRequestFactory -typealias DestinationOpenRequestHeaderFactory = () -> OkHttpStompDestination.DestinationOpenRequest -typealias MessageMetaDataFactory = (channel: Channel, message: Message) -> OkHttpStompDestination.MessageMetaData +typealias DestinationOpenRequestHeaderFactory = () -> StompDestination.DestinationOpenRequest +typealias MessageMetaDataFactory = (channel: Channel, message: Message) -> StompDestination.MessageMetaData -class OkHttpStompDestination( +/** + * + */ +class StompDestination( private val destination: String, private val openRequestFactory: DestinationOpenRequestHeaderFactory? = null, private val createMessageMetaDataCallable: MessageMetaDataFactory? = null @@ -19,7 +22,7 @@ class OkHttpStompDestination( override fun createChannelFactory() = SimpleChannelFactory { listener, parent -> require(parent is OkHttpStompMainChannel) - OkHttpStompMessageChannel(destination, parent, parent, listener) + StompMessageChannel(destination, parent, parent, listener) } override fun createOpenRequestFactory(channel: Channel) = SimpleProtocolOpenRequestFactory { diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompMessageChannel.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/StompMessageChannel.kt similarity index 74% rename from scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompMessageChannel.kt rename to scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/StompMessageChannel.kt index 0a98d7a3..11e20676 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompMessageChannel.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/StompMessageChannel.kt @@ -7,7 +7,7 @@ import com.tinder.scarlet.Protocol import com.tinder.scarlet.stomp.core.StompSender import com.tinder.scarlet.stomp.core.StompSubscriber -class OkHttpStompMessageChannel( +class StompMessageChannel( private val destination: String, private val stompSubscriber: StompSubscriber, private val stompSender: StompSender, @@ -17,14 +17,14 @@ class OkHttpStompMessageChannel( private var messageQueueListener: MessageQueue.Listener? = null override fun open(openRequest: Protocol.OpenRequest) { - val destinationOpenRequest = openRequest as OkHttpStompDestination.DestinationOpenRequest + val destinationOpenRequest = openRequest as StompDestination.DestinationOpenRequest val stompHeaders = destinationOpenRequest.headers stompSubscriber.subscribe(destination, stompHeaders) { message -> messageQueueListener?.onMessageReceived( channel = this, messageQueue = this, - message = Message.Text(message.payload.orEmpty()), - metadata = OkHttpStompDestination.MessageMetaData(message.headers) + message = Message.Text(message.payload.toString(Charsets.UTF_8)), + metadata = StompDestination.MessageMetaData(message.headers) ) } listener.onOpened(this) @@ -50,10 +50,13 @@ class OkHttpStompMessageChannel( messageMetaData: Protocol.MessageMetaData ): Boolean = when (message) { is Message.Text -> { - val metaData = messageMetaData as? OkHttpStompDestination.MessageMetaData + val metaData = messageMetaData as? StompDestination.MessageMetaData + stompSender.convertAndSend(message.value, destination, metaData?.headers) + } + is Message.Bytes -> { + val metaData = messageMetaData as? StompDestination.MessageMetaData stompSender.convertAndSend(message.value, destination, metaData?.headers) } - is Message.Bytes -> throw IllegalArgumentException("Bytes are not supported") } } From 2bc77244143696199a93cedca4f4970d71c334a3 Mon Sep 17 00:00:00 2001 From: NikitaZaltsman Date: Tue, 14 Jan 2020 01:04:26 +0600 Subject: [PATCH 19/32] Move settings to configuration class and rename main channel --- .../scarlet/stomp/okhttp/OkHttpStompClient.kt | 9 ++- .../scarlet/stomp/okhttp/StompDestination.kt | 2 +- ...tompMainChannel.kt => StompMainChannel.kt} | 56 ++++++++++--------- 3 files changed, 36 insertions(+), 31 deletions(-) rename scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/{OkHttpStompMainChannel.kt => StompMainChannel.kt} (84%) diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompClient.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompClient.kt index 07d5d6c5..3ba082df 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompClient.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompClient.kt @@ -8,14 +8,16 @@ import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.WebSocketListener -typealias ClientOpenRequestHeaderFactory = (Channel) -> OkHttpStompClient.ClientOpenRequest +typealias ClientOpenRequestHeaderFactory = (channel: Channel) -> OkHttpStompClient.ClientOpenRequest class OkHttpStompClient( + private val configuration: StompMainChannel.Configuration, private val okHttpClient: OkHttpClient, private val requestFactory: ClientOpenRequestHeaderFactory ) : Protocol { - override fun createChannelFactory() = OkHttpStompMainChannel.Factory( + override fun createChannelFactory() = StompMainChannel.Factory( + configuration, object : WebSocketFactory { override fun createWebSocket(request: Request, listener: WebSocketListener) { okHttpClient.newWebSocket(request, listener) @@ -34,10 +36,7 @@ class OkHttpStompClient( } data class ClientOpenRequest( - val host: String, val okHttpRequest: Request, - val heartbeatSendInterval: Long = 0, - val heartbeatReceiveInterval: Long = 0, val login: String? = null, val passcode: String? = null ) : Protocol.OpenRequest diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/StompDestination.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/StompDestination.kt index cf08d01e..e43fd4d8 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/StompDestination.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/StompDestination.kt @@ -21,7 +21,7 @@ class StompDestination( ) : Protocol { override fun createChannelFactory() = SimpleChannelFactory { listener, parent -> - require(parent is OkHttpStompMainChannel) + require(parent is StompMainChannel) StompMessageChannel(destination, parent, parent, listener) } diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompMainChannel.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/StompMainChannel.kt similarity index 84% rename from scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompMainChannel.kt rename to scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/StompMainChannel.kt index bc128948..2e8d0431 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompMainChannel.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/StompMainChannel.kt @@ -17,7 +17,8 @@ import java.util.UUID import java.util.concurrent.ConcurrentHashMap import kotlin.math.max -class OkHttpStompMainChannel( +class StompMainChannel( + private val configuration: Configuration, private val webSocketFactory: WebSocketFactory, private val listener: Channel.Listener ) : Channel, StompSender, StompSubscriber { @@ -36,15 +37,9 @@ class OkHttpStompMainChannel( private var messageHandler: MessageHandler? = null private var connection: Connection? = null - private var clientSendInterval: Long = 0 - private var clientReceiveInterval: Long = 0 - override fun open(openRequest: Protocol.OpenRequest) { val clientOpenRequest = openRequest as OkHttpStompClient.ClientOpenRequest - this.clientSendInterval = clientOpenRequest.heartbeatSendInterval - this.clientReceiveInterval = clientOpenRequest.heartbeatReceiveInterval - webSocketFactory.createWebSocket( clientOpenRequest.okHttpRequest, InnerWebSocketListener(clientOpenRequest) @@ -67,7 +62,6 @@ class OkHttpStompMainChannel( subscriptions.clear() sendDisconnectMessage() - connection?.close() connection = null @@ -149,6 +143,9 @@ class OkHttpStompMainChannel( private fun setupHeartBeat(stompMessage: StompMessage) { val (serverSendInterval, serverReceiveInterval) = stompMessage.headers.heartBeat + val clientSendInterval = configuration.heartbeatSendInterval + val clientReceiveInterval = configuration.heartbeatReceiveInterval + if (clientSendInterval > 0 && serverReceiveInterval > 0) { val interval = max(clientSendInterval, serverReceiveInterval) connection?.onWriteInactivity(interval) { sendHeartBeat() } @@ -159,7 +156,7 @@ class OkHttpStompMainChannel( connection?.onReadInactivity(interval) { sendErrorMessage("No messages received in $interval ms.") connection?.close() - listener.onFailed(this@OkHttpStompMainChannel, true, null) + listener.onFailed(this@StompMainChannel, true, null) } } @@ -190,10 +187,10 @@ class OkHttpStompMainChannel( override fun onOpen(webSocket: WebSocket, response: Response) { val webSocketConnection = WebSocketConnection(webSocket) - this@OkHttpStompMainChannel.connection = webSocketConnection - this@OkHttpStompMainChannel.messageHandler = webSocketConnection + this@StompMainChannel.connection = webSocketConnection + this@StompMainChannel.messageHandler = webSocketConnection - val host = openRequest.host + val host = configuration.host val login = openRequest.login val passcode = openRequest.passcode @@ -209,38 +206,39 @@ class OkHttpStompMainChannel( } override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { - listener.onClosing(this@OkHttpStompMainChannel) + listener.onClosing(this@StompMainChannel) } override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { - listener.onClosed(this@OkHttpStompMainChannel) - this@OkHttpStompMainChannel.connection = null + listener.onClosed(this@StompMainChannel) + this@StompMainChannel.connection = null } override fun onFailure(webSocket: WebSocket, throwable: Throwable, response: Response?) { - listener.onFailed(this@OkHttpStompMainChannel, true, throwable) - this@OkHttpStompMainChannel.connection = null + listener.onFailed(this@StompMainChannel, true, throwable) + this@StompMainChannel.connection = null } } private fun sendConnectMessage(host: String, login: String? = null, passcode: String? = null) { - val stompHeaders = StompHeaderAccessor.of() + val stompHeaderAccessor = StompHeaderAccessor.of() .apply { host(host) acceptVersion(ACCEPT_VERSION) login?.let(::login) passcode?.let(::passcode) + } - if (clientSendInterval > 0 && clientReceiveInterval > 0) { - heartBeat(clientSendInterval, clientReceiveInterval) - } + val clientSendInterval = configuration.heartbeatSendInterval + val clientReceiveInterval = configuration.heartbeatReceiveInterval - } - .createHeader() + if (clientSendInterval > 0 && clientReceiveInterval > 0) { + stompHeaderAccessor.heartBeat(clientSendInterval, clientReceiveInterval) + } val stompMessage = StompMessage.Builder() - .withHeaders(stompHeaders) + .withHeaders(stompHeaderAccessor.createHeader()) .create(StompCommand.CONNECT) connection?.send(stompMessage) @@ -253,7 +251,14 @@ class OkHttpStompMainChannel( connection?.send(stompMessage) } + data class Configuration( + val host: String, + val heartbeatSendInterval: Long = 0, + val heartbeatReceiveInterval: Long = 0 + ) + class Factory( + private val configuration: Configuration, private val webSocketFactory: WebSocketFactory ) : Channel.Factory { @@ -261,7 +266,8 @@ class OkHttpStompMainChannel( listener: Channel.Listener, parent: Channel? ): Channel? { - return OkHttpStompMainChannel( + return StompMainChannel( + configuration, webSocketFactory, listener ) From eb2929919523802943eb95d95a2808d75f70c65c Mon Sep 17 00:00:00 2001 From: NikitaZaltsman Date: Wed, 15 Jan 2020 22:50:54 +0600 Subject: [PATCH 20/32] Revert "Remove gazirra stomp client" This reverts commit 81e289a22351ab13c702101ea6f4eaeee5f2638f. --- scarlet-protocol-stomp/build.gradle | 1 + .../scarlet/stomp/GozirraStompClient.kt | 195 ++++++++++++++++++ 2 files changed, 196 insertions(+) create mode 100644 scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/GozirraStompClient.kt diff --git a/scarlet-protocol-stomp/build.gradle b/scarlet-protocol-stomp/build.gradle index 88a730b0..239d8406 100644 --- a/scarlet-protocol-stomp/build.gradle +++ b/scarlet-protocol-stomp/build.gradle @@ -8,6 +8,7 @@ apply plugin: 'java-library' dependencies { api project(':scarlet-core') + implementation files('libs/gozirra-client-0.4.1.jar') implementation project(':scarlet-core-internal') implementation rootProject.ext.rxJava diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/GozirraStompClient.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/GozirraStompClient.kt new file mode 100644 index 00000000..f09961dd --- /dev/null +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/GozirraStompClient.kt @@ -0,0 +1,195 @@ +/* + * © 2018 Match Group, LLC. + */ + +package com.tinder.scarlet.stomp + +import com.tinder.scarlet.Channel +import com.tinder.scarlet.Message +import com.tinder.scarlet.MessageQueue +import com.tinder.scarlet.Protocol +import com.tinder.scarlet.ProtocolSpecificEventAdapter +import com.tinder.scarlet.utils.SimpleChannelFactory +import com.tinder.scarlet.utils.SimpleProtocolOpenRequestFactory +import net.ser1.stomp.Client +import javax.security.auth.login.LoginException + +class GozirraStompClient( + private val openRequestFactory: RequestFactory +) : Protocol { + + override fun createChannelFactory(): Channel.Factory { + return SimpleChannelFactory { listener, _ -> + StompMainChannel(listener) + } + } + + override fun createOpenRequestFactory(channel: Channel): Protocol.OpenRequest.Factory { + return SimpleProtocolOpenRequestFactory { + openRequestFactory.createClientOpenRequest() + } + } + + override fun createEventAdapterFactory(): ProtocolSpecificEventAdapter.Factory { + return object : ProtocolSpecificEventAdapter.Factory {} + } + + interface RequestFactory { + fun createClientOpenRequest(): ClientOpenRequest + } + + open class SimpleRequestFactory( + private val createClientOpenRequestCallable: () -> ClientOpenRequest + ) : RequestFactory { + override fun createClientOpenRequest(): ClientOpenRequest { + return createClientOpenRequestCallable() + } + } + + data class ClientOpenRequest( + val url: String, + val port: Int, + val login: String, + val password: String + ) : Protocol.OpenRequest + + data class MessageMetaData(val headers: Map) : Protocol.MessageMetaData +} + +class GozirraStompDestination( + val destination: String, + private val openRequestFactory: RequestFactory +) : Protocol { + + override fun createChannelFactory(): Channel.Factory { + return SimpleChannelFactory { listener, parent -> + require(parent is StompMainChannel) + StompMessageChannel(parent as StompMainChannel, destination, listener) + } + } + + override fun createOpenRequestFactory(channel: Channel): Protocol.OpenRequest.Factory { + return SimpleProtocolOpenRequestFactory { + DestinationOpenRequest( + openRequestFactory.createDestinationOpenRequestHeader(destination) + ) + } + } + + override fun createEventAdapterFactory(): ProtocolSpecificEventAdapter.Factory { + return object : ProtocolSpecificEventAdapter.Factory {} + } + + interface RequestFactory { + fun createDestinationOpenRequestHeader(destination: String): Map + } + + open class SimpleRequestFactory( + private val createDestinationOpenRequestHeaderCallable: (String) -> Map + ) : RequestFactory { + + override fun createDestinationOpenRequestHeader(destination: String): Map { + return createDestinationOpenRequestHeaderCallable(destination) + } + } + + data class DestinationOpenRequest( + val headers: Map + ) : Protocol.OpenRequest +} + +class StompMainChannel( + private val listener: Channel.Listener +) : Channel { + var client: Client? = null + + override fun open(openRequest: Protocol.OpenRequest) { + val (url, port, login, password) = openRequest as GozirraStompClient.ClientOpenRequest + try { + val client = Client(url, port, login, password) + client.addErrorListener { _, _ -> + listener.onFailed(this, true, null) + } + this.client = client + listener.onOpened(this) + } catch (e: LoginException) { + listener.onFailed(this, true, e) + } catch (e: Throwable) { + listener.onFailed(this, true, e) + } + } + + override fun close(closeRequest: Protocol.CloseRequest) { + forceClose() + } + + override fun forceClose() { + try { + client?.disconnect() + client = null + listener.onClosed(this) + } catch (e: LoginException) { + listener.onFailed(this, true, e) + } catch (e: Throwable) { + listener.onFailed(this, true, e) + } + } + + override fun createMessageQueue(listener: MessageQueue.Listener): MessageQueue? { + return null + } +} + +class StompMessageChannel( + private val mainChannel: StompMainChannel, + private val destination: String, + private val listener: Channel.Listener +) : Channel, MessageQueue { + + private var client: Client? = null + private var messageQueueListener: MessageQueue.Listener? = null + + override fun open(openRequest: Protocol.OpenRequest) { + val destinationOpenRequest = openRequest as GozirraStompDestination.DestinationOpenRequest + client = mainChannel.client + client?.subscribe( + destination, + { headers, message -> + messageQueueListener?.onMessageReceived( + this, + this, + Message.Text(message), + GozirraStompClient.MessageMetaData(headers as Map) + ) + }, + // This map is modified by gozirra internally + destinationOpenRequest.headers.toMutableMap() + ) + listener.onOpened(this) + } + + override fun close(closeRequest: Protocol.CloseRequest) { + forceClose() + } + + override fun forceClose() { + client?.unsubscribe(destination) + client = null + listener.onClosed(this) + } + + override fun createMessageQueue(listener: MessageQueue.Listener): MessageQueue { + require(messageQueueListener == null) + messageQueueListener = listener + return this + } + + override fun send(message: Message, messageMetaData: Protocol.MessageMetaData): Boolean { + val client = client ?: return false + when (message) { + is Message.Text -> client.send(destination, message.value) + is Message.Bytes -> throw IllegalArgumentException("Bytes are not supported") + } + return true + } +} From 18df82b92f4ce1617dc03626ebc6cbe13dde5ef4 Mon Sep 17 00:00:00 2001 From: NikitaZaltsman Date: Wed, 15 Jan 2020 23:23:32 +0600 Subject: [PATCH 21/32] Add documentation for StomHeader Refactoring packages Refactoring and add documentation update stomp message encoder refactoring and update code style add method for sent byte array to stomp sender make ClientOpenRequestHeaderFactory typealias as private fix code style --- .../stomp/{okhttp => core}/Connection.kt | 9 +- .../{okhttp => core}/StompDestination.kt | 12 +- .../tinder/scarlet/stomp/core/StompHeader.kt | 61 -------- .../{okhttp => core}/StompMainChannel.kt | 64 +++++--- .../{okhttp => core}/StompMessageChannel.kt | 5 +- .../tinder/scarlet/stomp/core/StompSender.kt | 5 +- .../scarlet/stomp/core/StompSubscriber.kt | 10 +- .../stomp/core/{ => models}/StompCommand.kt | 8 +- .../scarlet/stomp/core/models/StompHeader.kt | 108 +++++++++++++ .../stomp/core/{ => models}/StompMessage.kt | 32 ++-- .../scarlet/stomp/okhttp/MessageHandler.kt | 7 +- .../scarlet/stomp/okhttp/OkHttpStompClient.kt | 4 +- .../stomp/okhttp/WebSocketConnection.kt | 24 +-- .../stomp/support/StompHeaderAccessor.kt | 18 ++- .../stomp/support/StompMessageDecoder.kt | 4 +- .../stomp/support/StompMessageEncoder.kt | 148 ++++++++---------- .../stomp/support/StompMessageEncoderTest.kt | 96 +++++++++--- 17 files changed, 372 insertions(+), 243 deletions(-) rename scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/{okhttp => core}/Connection.kt (77%) rename scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/{okhttp => core}/StompDestination.kt (80%) delete mode 100644 scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompHeader.kt rename scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/{okhttp => core}/StompMainChannel.kt (83%) rename scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/{okhttp => core}/StompMessageChannel.kt (93%) rename scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/{ => models}/StompCommand.kt (81%) create mode 100644 scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/models/StompHeader.kt rename scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/{ => models}/StompMessage.kt (60%) diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/Connection.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/Connection.kt similarity index 77% rename from scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/Connection.kt rename to scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/Connection.kt index 69bea05e..c197c948 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/Connection.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/Connection.kt @@ -1,6 +1,6 @@ -package com.tinder.scarlet.stomp.okhttp +package com.tinder.scarlet.stomp.core -import com.tinder.scarlet.stomp.core.StompMessage +import com.tinder.scarlet.stomp.core.models.StompMessage interface Connection { @@ -9,14 +9,14 @@ interface Connection { * @param message the message * @return true if the message was enqueued. */ - fun send(message: StompMessage): Boolean + fun sendMessage(message: StompMessage): Boolean /** * Register a task to invoke after a period of read inactivity. * @param runnable the task to invoke * @param duration the amount of inactive time in milliseconds */ - fun onReadInactivity(duration: Long, runnable: () -> Unit) + fun onReceiveInactivity(duration: Long, runnable: () -> Unit) /** * Register a task to invoke after a period of write inactivity. @@ -34,5 +34,4 @@ interface Connection { * Close the connection. */ fun close() - } \ No newline at end of file diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/StompDestination.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompDestination.kt similarity index 80% rename from scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/StompDestination.kt rename to scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompDestination.kt index e43fd4d8..b2ede48a 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/StompDestination.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompDestination.kt @@ -1,15 +1,15 @@ -package com.tinder.scarlet.stomp.okhttp +package com.tinder.scarlet.stomp.core import com.tinder.scarlet.Channel import com.tinder.scarlet.Message import com.tinder.scarlet.Protocol import com.tinder.scarlet.ProtocolSpecificEventAdapter -import com.tinder.scarlet.stomp.core.StompHeader +import com.tinder.scarlet.stomp.core.models.StompHeader import com.tinder.scarlet.utils.SimpleChannelFactory import com.tinder.scarlet.utils.SimpleProtocolOpenRequestFactory -typealias DestinationOpenRequestHeaderFactory = () -> StompDestination.DestinationOpenRequest -typealias MessageMetaDataFactory = (channel: Channel, message: Message) -> StompDestination.MessageMetaData +private typealias DestinationOpenRequestHeaderFactory = (channel: Channel) -> StompDestination.DestinationOpenRequest +private typealias MessageMetaDataFactory = (channel: Channel, message: Message) -> StompDestination.MessageMetaData /** * @@ -26,7 +26,7 @@ class StompDestination( } override fun createOpenRequestFactory(channel: Channel) = SimpleProtocolOpenRequestFactory { - openRequestFactory?.invoke() ?: Protocol.OpenRequest.Empty + openRequestFactory?.invoke(channel) ?: Protocol.OpenRequest.Empty } override fun createOutgoingMessageMetaDataFactory( @@ -46,7 +46,6 @@ class StompDestination( message: Message ) = createMessageMetaDataCallable?.invoke(channel, message) ?: Protocol.MessageMetaData.Empty - } data class MessageMetaData( @@ -56,5 +55,4 @@ class StompDestination( data class DestinationOpenRequest( val headers: StompHeader ) : Protocol.OpenRequest - } \ No newline at end of file diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompHeader.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompHeader.kt deleted file mode 100644 index be820c82..00000000 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompHeader.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.tinder.scarlet.stomp.core - -class StompHeader( - private val headers: Map -) : Map by headers { - - companion object { - - // Standard headers (as defined in the spec) - const val CONTENT_TYPE = "content-type" // SEND, MESSAGE, ERROR - - const val CONTENT_LENGTH = "content-length" // SEND, MESSAGE, ERROR - - const val RECEIPT = "receipt" // any client frame other than CONNECT - - // CONNECT - const val HOST = "host" - - const val ACCEPT_VERSION = "accept-version" - - const val LOGIN = "login" - - const val PASSCODE = "passcode" - - const val HEARTBEAT = "heart-beat" - - // CONNECTED - - const val SESSION = "session" - - const val SERVER = "server" - - // SEND - - const val DESTINATION = "destination" - - // SUBSCRIBE, UNSUBSCRIBE - const val ID = "id" - - const val ACK = "ack" - - // MESSAGE - const val SUBSCRIPTION = "subscription" - - const val MESSAGE_ID = "message-id" - - // RECEIPT - const val RECEIPT_ID = "receipt-id" - - } - - val destination: String? - get() = headers[DESTINATION] - - val heartBeat: Pair - get() { - val heartBeat = headers[HEARTBEAT] ?: return 0L to 0L - val (sendInterval, receiveInterval) = heartBeat.split(",") - return sendInterval.toLong() to receiveInterval.toLong() - } -} \ No newline at end of file diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/StompMainChannel.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompMainChannel.kt similarity index 83% rename from scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/StompMainChannel.kt rename to scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompMainChannel.kt index 2e8d0431..e04825bc 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/StompMainChannel.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompMainChannel.kt @@ -1,13 +1,14 @@ -package com.tinder.scarlet.stomp.okhttp +package com.tinder.scarlet.stomp.core import com.tinder.scarlet.Channel import com.tinder.scarlet.Protocol -import com.tinder.scarlet.stomp.core.StompCommand -import com.tinder.scarlet.stomp.core.StompHeader -import com.tinder.scarlet.stomp.core.StompListener -import com.tinder.scarlet.stomp.core.StompMessage -import com.tinder.scarlet.stomp.core.StompSender -import com.tinder.scarlet.stomp.core.StompSubscriber +import com.tinder.scarlet.stomp.core.models.StompCommand +import com.tinder.scarlet.stomp.core.models.StompHeader +import com.tinder.scarlet.stomp.core.models.StompMessage +import com.tinder.scarlet.stomp.okhttp.MessageHandler +import com.tinder.scarlet.stomp.okhttp.OkHttpStompClient +import com.tinder.scarlet.stomp.okhttp.WebSocketConnection +import com.tinder.scarlet.stomp.okhttp.WebSocketFactory import com.tinder.scarlet.stomp.support.StompHeaderAccessor import okhttp3.Response import okhttp3.WebSocket @@ -17,6 +18,9 @@ import java.util.UUID import java.util.concurrent.ConcurrentHashMap import kotlin.math.max +/** + * + */ class StompMainChannel( private val configuration: Configuration, private val webSocketFactory: WebSocketFactory, @@ -68,6 +72,23 @@ class StompMainChannel( messageHandler = null } + override fun convertAndSend( + payload: ByteArray, + destination: String, + headers: StompHeader? + ): Boolean { + val stompHeaders = StompHeaderAccessor.of(headers.orEmpty()) + .apply { destination(destination) } + .createHeader() + + val stompMessage = StompMessage.Builder() + .withPayload(payload) + .withHeaders(stompHeaders) + .create(StompCommand.SEND) + + return connection?.sendMessage(stompMessage) ?: false + } + override fun convertAndSend( payload: String, destination: String, @@ -82,7 +103,7 @@ class StompMainChannel( .withHeaders(stompHeaders) .create(StompCommand.SEND) - return connection?.send(stompMessage) ?: false + return connection?.sendMessage(stompMessage) ?: false } override fun subscribe( @@ -104,7 +125,7 @@ class StompMainChannel( .withHeaders(stompHeaders) .create(StompCommand.SUBSCRIBE) - connection?.send(stompMessage) + connection?.sendMessage(stompMessage) topicIds[destination] = generateId subscriptions[destination] = listener @@ -122,7 +143,7 @@ class StompMainChannel( .withHeaders(stompHeaders) .create(StompCommand.UNSUBSCRIBE) - connection?.send(stompMessage) + connection?.sendMessage(stompMessage) subscriptions.remove(destination) } @@ -137,7 +158,7 @@ class StompMainChannel( listener?.invoke(stompMessage) } StompCommand.ERROR -> listener.onFailed(this, true, null) - else -> Unit //not a server message + else -> Unit // not a server message } private fun setupHeartBeat(stompMessage: StompMessage) { @@ -148,25 +169,24 @@ class StompMainChannel( if (clientSendInterval > 0 && serverReceiveInterval > 0) { val interval = max(clientSendInterval, serverReceiveInterval) - connection?.onWriteInactivity(interval) { sendHeartBeat() } + connection?.onWriteInactivity(interval, ::sendHeartBeat) } if (clientReceiveInterval > 0 && serverSendInterval > 0) { - val interval = max(clientReceiveInterval, serverSendInterval) * HEARTBEAT_MULTIPLIER; - connection?.onReadInactivity(interval) { + val interval = max(clientReceiveInterval, serverSendInterval) * HEARTBEAT_MULTIPLIER + connection?.onReceiveInactivity(interval) { sendErrorMessage("No messages received in $interval ms.") connection?.close() listener.onFailed(this@StompMainChannel, true, null) } } - } private fun sendHeartBeat() { val stompMessage = StompMessage.Builder() .create(StompCommand.UNKNOWN) - connection?.send(stompMessage) + connection?.sendMessage(stompMessage) } private fun sendErrorMessage(error: String) { @@ -178,7 +198,7 @@ class StompMainChannel( .withHeaders(headers) .create(StompCommand.ERROR) - connection?.send(stompMessage) + connection?.sendMessage(stompMessage) } inner class InnerWebSocketListener( @@ -198,11 +218,11 @@ class StompMainChannel( } override fun onMessage(webSocket: WebSocket, bytes: ByteString) { - messageHandler?.handle(bytes.utf8())?.let(::handleIncome) + messageHandler?.handle(bytes.toByteArray())?.let(::handleIncome) } override fun onMessage(webSocket: WebSocket, text: String) { - messageHandler?.handle(text)?.let(::handleIncome) + messageHandler?.handle(text.toByteArray(Charsets.UTF_8))?.let(::handleIncome) } override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { @@ -218,7 +238,6 @@ class StompMainChannel( listener.onFailed(this@StompMainChannel, true, throwable) this@StompMainChannel.connection = null } - } private fun sendConnectMessage(host: String, login: String? = null, passcode: String? = null) { @@ -241,14 +260,14 @@ class StompMainChannel( .withHeaders(stompHeaderAccessor.createHeader()) .create(StompCommand.CONNECT) - connection?.send(stompMessage) + connection?.sendMessage(stompMessage) } private fun sendDisconnectMessage() { val stompMessage = StompMessage.Builder() .create(StompCommand.DISCONNECT) - connection?.send(stompMessage) + connection?.sendMessage(stompMessage) } data class Configuration( @@ -273,5 +292,4 @@ class StompMainChannel( ) } } - } diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/StompMessageChannel.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompMessageChannel.kt similarity index 93% rename from scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/StompMessageChannel.kt rename to scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompMessageChannel.kt index 11e20676..cb429f95 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/StompMessageChannel.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompMessageChannel.kt @@ -1,11 +1,9 @@ -package com.tinder.scarlet.stomp.okhttp +package com.tinder.scarlet.stomp.core import com.tinder.scarlet.Channel import com.tinder.scarlet.Message import com.tinder.scarlet.MessageQueue import com.tinder.scarlet.Protocol -import com.tinder.scarlet.stomp.core.StompSender -import com.tinder.scarlet.stomp.core.StompSubscriber class StompMessageChannel( private val destination: String, @@ -58,5 +56,4 @@ class StompMessageChannel( stompSender.convertAndSend(message.value, destination, metaData?.headers) } } - } diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompSender.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompSender.kt index bc72b8d0..d79fdcba 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompSender.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompSender.kt @@ -1,7 +1,10 @@ package com.tinder.scarlet.stomp.core +import com.tinder.scarlet.stomp.core.models.StompHeader + interface StompSender { - fun convertAndSend(payload: String, destination: String, headers: StompHeader?): Boolean + fun convertAndSend(payload: ByteArray, destination: String, headers: StompHeader?): Boolean + fun convertAndSend(payload: String, destination: String, headers: StompHeader?): Boolean } \ No newline at end of file diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompSubscriber.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompSubscriber.kt index 5e094a03..63405109 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompSubscriber.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompSubscriber.kt @@ -1,11 +1,19 @@ package com.tinder.scarlet.stomp.core +import com.tinder.scarlet.stomp.core.models.StompHeader +import com.tinder.scarlet.stomp.core.models.StompMessage + typealias StompListener = (StompMessage) -> Unit interface StompSubscriber { + /** + * + */ fun subscribe(destination: String, headers: StompHeader?, listener: StompListener) + /** + * + */ fun unsubscribe(destination: String) - } \ No newline at end of file diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompCommand.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/models/StompCommand.kt similarity index 81% rename from scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompCommand.kt rename to scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/models/StompCommand.kt index e7ef16f1..c43c1715 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompCommand.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/models/StompCommand.kt @@ -1,21 +1,21 @@ -package com.tinder.scarlet.stomp.core +package com.tinder.scarlet.stomp.core.models enum class StompCommand( val isBodyAllowed: Boolean = false, val isDestinationRequired: Boolean = false ) { - //client + // client CONNECT, DISCONNECT, SEND(isBodyAllowed = true, isDestinationRequired = true), SUBSCRIBE(isDestinationRequired = true), UNSUBSCRIBE, - //server + // server CONNECTED, MESSAGE(isBodyAllowed = true, isDestinationRequired = true), ERROR(isBodyAllowed = true), - //heartbeat + // heartbeat UNKNOWN } \ No newline at end of file diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/models/StompHeader.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/models/StompHeader.kt new file mode 100644 index 00000000..41ae250b --- /dev/null +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/models/StompHeader.kt @@ -0,0 +1,108 @@ +/* + * © 2018 Match Group, LLC. + */ + +package com.tinder.scarlet.stomp.core.models + +/** + * Represents STOMP frame headers. + */ +class StompHeader( + private val headers: Map +) : Map by headers { + + companion object { + + // Standard Headers + /** + * The implied text encoding for MIME types starting with text/ is UTF-8. + * If you are using a text based MIME type with a different encoding then you SHOULD append ;charset= to the MIME type. + * For example, text/html;charset=utf-16 SHOULD be used if your sending an HTML body in UTF-16 encoding. + */ + const val CONTENT_TYPE = "content-type" + /** + * This header is an octet count for the length of the message body. + */ + const val CONTENT_LENGTH = "content-length" + /** + * This will cause the server to acknowledge the processing of the client frame with a RECEIPT frame + */ + const val RECEIPT = "receipt" + /** + * The name of a virtual host that the client wishes to connect to. + */ + const val HOST = "host" + /** + * The versions of the STOMP protocol the client supports. + */ + const val ACCEPT_VERSION = "accept-version" + /** + * The user identifier used to authenticate against a secured STOMP server. + */ + const val LOGIN = "login" + /** + * The password used to authenticate against a secured STOMP server. + */ + const val PASSCODE = "passcode" + /** + * Heart-beating can optionally be used to test the healthiness of the underlying TCP + * connection and to make sure that the remote end is alive and kicking. + * When used, the heart-beat header MUST contain two positive integers separated by a comma. + * + * For detail information see https://stomp.github.io + */ + const val HEARTBEAT = "heart-beat" + /** + * A session identifier that uniquely identifies the session. + */ + const val SESSION = "session" + /** + * A field that contains information about the STOMP server. + * The field MUST contain a server-name field and MAY be followed by optional comment fields delimited by a space character. + */ + const val SERVER = "server" + + /** + * The header indicates where to send the message. + */ + const val DESTINATION = "destination" + /** + * Since a single connection can have multiple open subscriptions with a server, + * an id header MUST be included in the frame to uniquely identify the subscription. + * The id header allows the client and server to relate subsequent MESSAGE or UNSUBSCRIBE frames to the original subscription. + */ + const val ID = "id" + /** + * The valid values for the ack header are auto, client, or client-individual. + * If the header is not set, it defaults to auto. + * + * For detail information see https://stomp.github.io + */ + const val ACK = "ack" + /** + * The header matching the identifier of the subscription that is receiving the message. + */ + const val SUBSCRIPTION = "subscription" + /** + * The header with a unique identifier for that message. + */ + const val MESSAGE_ID = "message-id" + /** + * The header receipt-id, where the value is the value of the receipt header in the frame which this is a receipt for. + */ + const val RECEIPT_ID = "receipt-id" + } + + val destination: String? + get() = headers[DESTINATION] + + /** + * Get heartBeat in pair format where first value is sendInterval, second value is receiveInterval. + */ + val heartBeat: Pair + get() { + val heartBeat = headers[HEARTBEAT] ?: return 0L to 0L + val (sendInterval, receiveInterval) = heartBeat.split(",") + return sendInterval.toLong() to receiveInterval.toLong() + } +} \ No newline at end of file diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompMessage.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/models/StompMessage.kt similarity index 60% rename from scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompMessage.kt rename to scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/models/StompMessage.kt index c8d2fdec..dbbb13bd 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompMessage.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/models/StompMessage.kt @@ -1,20 +1,28 @@ -package com.tinder.scarlet.stomp.core +package com.tinder.scarlet.stomp.core.models import com.tinder.scarlet.stomp.support.StompHeaderAccessor +/** + * + */ class StompMessage private constructor( val command: StompCommand, - val payload: String?, + val payload: ByteArray, val headers: StompHeader ) { class Builder { - private var payload: String? = null + private var payload: ByteArray = ByteArray(0) private var headers: StompHeaderAccessor = StompHeaderAccessor.of() - fun withPayload(payload: String?): Builder { - this.payload = payload + fun withPayload(payload: ByteArray): Builder { + this.payload = payload.copyOf() + return this + } + + fun withPayload(payload: String): Builder { + this.payload = payload.toByteArray() return this } @@ -26,10 +34,13 @@ class StompMessage private constructor( fun create(command: StompCommand): StompMessage { val createHeader = headers.createHeader() if (command.isDestinationRequired) check(!createHeader.destination.isNullOrEmpty()) { "Command $command required destination" } - if (!command.isBodyAllowed) check(payload.isNullOrEmpty()) { "Command $command doesn't support body" } - return StompMessage(command, payload, createHeader) + if (!command.isBodyAllowed) check(payload.isEmpty()) { "Command $command doesn't support body" } + return StompMessage( + command = command, + payload = payload, + headers = createHeader + ) } - } override fun equals(other: Any?): Boolean { @@ -39,7 +50,7 @@ class StompMessage private constructor( other as StompMessage if (command != other.command) return false - if (payload != other.payload) return false + if (!payload.contentEquals(other.payload)) return false if (headers != other.headers) return false return true @@ -47,9 +58,8 @@ class StompMessage private constructor( override fun hashCode(): Int { var result = command.hashCode() - result = 31 * result + (payload?.hashCode() ?: 0) + result = 31 * result + payload.contentHashCode() result = 31 * result + headers.hashCode() return result } - } \ No newline at end of file diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/MessageHandler.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/MessageHandler.kt index c258315c..08b67446 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/MessageHandler.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/MessageHandler.kt @@ -1,12 +1,11 @@ package com.tinder.scarlet.stomp.okhttp -import com.tinder.scarlet.stomp.core.StompMessage +import com.tinder.scarlet.stomp.core.models.StompMessage interface MessageHandler { /** - * Convert given raw data string to stomp message + * Convert given raw data byte array to stomp message */ - fun handle(data: String): StompMessage - + fun handle(data: ByteArray): StompMessage } \ No newline at end of file diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompClient.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompClient.kt index 3ba082df..06d54ebf 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompClient.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompClient.kt @@ -3,12 +3,13 @@ package com.tinder.scarlet.stomp.okhttp import com.tinder.scarlet.Channel import com.tinder.scarlet.Protocol import com.tinder.scarlet.ProtocolSpecificEventAdapter +import com.tinder.scarlet.stomp.core.StompMainChannel import com.tinder.scarlet.utils.SimpleProtocolOpenRequestFactory import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.WebSocketListener -typealias ClientOpenRequestHeaderFactory = (channel: Channel) -> OkHttpStompClient.ClientOpenRequest +private typealias ClientOpenRequestHeaderFactory = (channel: Channel) -> OkHttpStompClient.ClientOpenRequest class OkHttpStompClient( private val configuration: StompMainChannel.Configuration, @@ -40,5 +41,4 @@ class OkHttpStompClient( val login: String? = null, val passcode: String? = null ) : Protocol.OpenRequest - } \ No newline at end of file diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/WebSocketConnection.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/WebSocketConnection.kt index ef1b4be9..292401ae 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/WebSocketConnection.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/WebSocketConnection.kt @@ -1,13 +1,14 @@ package com.tinder.scarlet.stomp.okhttp -import com.tinder.scarlet.stomp.core.StompMessage +import com.tinder.scarlet.stomp.core.Connection +import com.tinder.scarlet.stomp.core.models.StompMessage import com.tinder.scarlet.stomp.support.StompMessageDecoder import com.tinder.scarlet.stomp.support.StompMessageEncoder import okhttp3.WebSocket +import okio.ByteString import java.util.concurrent.Executors import java.util.concurrent.TimeUnit - class WebSocketConnection( private val webSocket: WebSocket ) : Connection, MessageHandler { @@ -20,23 +21,26 @@ class WebSocketConnection( private val executor = Executors.newSingleThreadScheduledExecutor() + private val messageEncoder = StompMessageEncoder() + private val messageDecoder = StompMessageDecoder() + companion object { private const val NORMAL_CLOSURE_STATUS_CODE = 1000 private const val NORMAL_CLOSURE_REASON = "Normal closure" - } - override fun send(message: StompMessage): Boolean { + override fun sendMessage(message: StompMessage): Boolean { val lastWriteTime = lastWriteTime if (lastWriteTime != -1L) { this.lastWriteTime = System.currentTimeMillis() } - val encodedMessage = StompMessageEncoder.encode(message) - return webSocket.send(encodedMessage) + val encodedMessage = messageEncoder.encode(message) + val byteString = ByteString.of(encodedMessage, 0, encodedMessage.size) + return webSocket.send(byteString) } - override fun onReadInactivity(duration: Long, runnable: () -> Unit) { + override fun onReceiveInactivity(duration: Long, runnable: () -> Unit) { lastReadTime = System.currentTimeMillis() executor.scheduleWithFixedDelay({ if (System.currentTimeMillis() - lastReadTime > duration) { @@ -64,13 +68,11 @@ class WebSocketConnection( executor.shutdown() } - override fun handle(data: String): StompMessage { + override fun handle(data: ByteArray): StompMessage { val lastReadTime = lastReadTime if (lastReadTime != -1L) { this.lastReadTime = System.currentTimeMillis() } - return StompMessageDecoder.decode(data) + return messageDecoder.decode(data) } - - } \ No newline at end of file diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompHeaderAccessor.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompHeaderAccessor.kt index cc0502b2..fabc1360 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompHeaderAccessor.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompHeaderAccessor.kt @@ -1,19 +1,18 @@ package com.tinder.scarlet.stomp.support -import com.tinder.scarlet.stomp.core.StompHeader +import com.tinder.scarlet.stomp.core.models.StompHeader class StompHeaderAccessor private constructor(headers: Map) { companion object { + /** + * Custom header for the server to notify anything. + */ private const val STOMP_MESSAGE_HEADER = "message" - fun of( - headers: Map = emptyMap() - ): StompHeaderAccessor { - return StompHeaderAccessor(headers) - } - + fun of(headers: Map = emptyMap()): StompHeaderAccessor = + StompHeaderAccessor(headers) } private val mutableHeaders = mutableMapOf().apply { @@ -48,6 +47,10 @@ class StompHeaderAccessor private constructor(headers: Map) { mutableHeaders[StompHeader.CONTENT_TYPE] = contentType } + fun contentLength(length: Int) { + mutableHeaders[StompHeader.CONTENT_LENGTH] = length.toString() + } + fun host(host: String) { mutableHeaders[StompHeader.HOST] = host } @@ -65,5 +68,4 @@ class StompHeaderAccessor private constructor(headers: Map) { } fun createHeader() = StompHeader(mutableHeaders) - } \ No newline at end of file diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompMessageDecoder.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompMessageDecoder.kt index 08b39450..6504e129 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompMessageDecoder.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompMessageDecoder.kt @@ -1,7 +1,7 @@ package com.tinder.scarlet.stomp.support -import com.tinder.scarlet.stomp.core.StompCommand -import com.tinder.scarlet.stomp.core.StompMessage +import com.tinder.scarlet.stomp.core.models.StompCommand +import com.tinder.scarlet.stomp.core.models.StompMessage import com.tinder.scarlet.stomp.support.StompMessageEncoder.TERMINATE_MESSAGE_SYMBOL import java.io.StringReader import java.util.Scanner diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompMessageEncoder.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompMessageEncoder.kt index 64841e59..9746b797 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompMessageEncoder.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompMessageEncoder.kt @@ -1,75 +1,87 @@ package com.tinder.scarlet.stomp.support -import com.tinder.scarlet.stomp.core.StompCommand -import com.tinder.scarlet.stomp.core.StompHeader -import com.tinder.scarlet.stomp.core.StompMessage +import com.tinder.scarlet.stomp.core.models.StompCommand +import com.tinder.scarlet.stomp.core.models.StompHeader +import com.tinder.scarlet.stomp.core.models.StompMessage +import java.io.ByteArrayOutputStream +import java.io.DataOutputStream +class StompMessageEncoder { -object StompMessageEncoder { + companion object { - private const val LF = '\n' - private const val COLON = ':' + private const val LF = '\n'.toInt() + private const val COLON = ':'.toInt() - private const val HEARTBEAT_PAYLOAD = "\n" - const val TERMINATE_MESSAGE_SYMBOL = '\u0000' + private const val HEARTBEAT_PAYLOAD = "\n" + private const val START_HEADERS_SIZE = 64 + } - fun encode(stompMessage: StompMessage): String { - val stringBuilder = StringBuilder() + fun encode(stompMessage: StompMessage): ByteArray { + val arraySize = START_HEADERS_SIZE + stompMessage.payload.size + val arrayOutputStream = ByteArrayOutputStream(arraySize) - val command = stompMessage.command - if (command == StompCommand.UNKNOWN) { - stringBuilder.append(HEARTBEAT_PAYLOAD) - } else { - val headers = writeHeaders(command, stompMessage.headers, stompMessage.payload.orEmpty()) - stringBuilder - .append(command.toString()) - .append(LF) - .append(headers) - .append(LF) - .apply { stompMessage.payload?.let(::append) } - .append(TERMINATE_MESSAGE_SYMBOL) + DataOutputStream(arrayOutputStream).use { dataOutputStream -> + + val command = stompMessage.command + if (command != StompCommand.UNKNOWN) { + writeMessage(dataOutputStream, command, stompMessage) + } else { + dataOutputStream.writeChars(HEARTBEAT_PAYLOAD) + } } - return stringBuilder.toString() + return arrayOutputStream.toByteArray() } - private fun writeHeaders( + private fun writeMessage( + dataOutputStream: DataOutputStream, command: StompCommand, - headers: Map, - payload: String - ): String { - if (headers.isEmpty()) { - return "" - } + stompMessage: StompMessage + ) = with(dataOutputStream) { + write(command.toString().toByteArray(Charsets.UTF_8)) + write(LF) + writeHeaders(stompMessage, dataOutputStream) + write(LF) + write(stompMessage.payload) + writeByte(0) + } + + private fun writeHeaders(stompMessage: StompMessage, outputStream: DataOutputStream) { + if (stompMessage.headers.isEmpty()) return + + val command = stompMessage.command + val headers = stompMessage.headers val shouldEscape = command != StompCommand.CONNECT && command != StompCommand.CONNECTED - val stringBuilder = StringBuilder() headers.forEach { (key, value) -> - if (command.isBodyAllowed && key == StompHeader.CONTENT_LENGTH) { - return@forEach - } + if (command.isBodyAllowed && key == StompHeader.CONTENT_LENGTH) return@forEach val headerKey = encode(key, shouldEscape) val headerValue = encode(value, shouldEscape) - stringBuilder - .append(headerKey) - .append(COLON) - .append(headerValue) - .append(LF) + with(outputStream) { + write(headerKey) + write(COLON) + write(headerValue) + write(LF) + } } + if (command.isBodyAllowed) { - val contentLength = payload.length - stringBuilder.append(StompHeader.CONTENT_LENGTH) - .append(COLON) - .append(contentLength) - .append(LF) + val contentLength = stompMessage.payload.size + with(outputStream) { + write(StompHeader.CONTENT_LENGTH.toByteArray(Charsets.UTF_8)) + write(COLON) + write(contentLength.toString().toByteArray(Charsets.UTF_8)) + write(LF) + } } - return stringBuilder.toString() } - private fun encode(input: String, escape: Boolean): String { - return if (escape) escape(input) else input + private fun encode(input: String, escape: Boolean): ByteArray { + val outputString = if (escape) escape(input) else input + return outputString.toByteArray(Charsets.UTF_8) } /** @@ -77,42 +89,16 @@ object StompMessageEncoder { * "Value Encoding". */ private fun escape(inString: String): String { - var sb: StringBuilder? = null - inString.forEachIndexed { index, symbol -> + val stringBuilder = StringBuilder(inString.length) + inString.forEach { symbol -> when (symbol) { - '\\' -> { - sb = getStringBuilder(sb, inString, index) - sb?.append("\\\\") - } - ':' -> { - sb = getStringBuilder(sb, inString, index) - sb?.append("\\c") - } - '\n' -> { - sb = getStringBuilder(sb, inString, index) - sb?.append("\\n") - } - '\r' -> { - sb = getStringBuilder(sb, inString, index) - sb?.append("\\r") - } - else -> sb?.append(symbol) + '\\' -> stringBuilder.append("\\\\") + ':' -> stringBuilder.append("\\c") + '\n' -> stringBuilder.append("\\n") + '\r' -> stringBuilder.append("\\r") + else -> stringBuilder.append(symbol) } } - return sb?.toString() ?: inString - } - - private fun getStringBuilder( - stringBuilder: StringBuilder?, - inString: String, - index: Int - ): StringBuilder { - var sb: StringBuilder? = stringBuilder - if (sb == null) { - sb = java.lang.StringBuilder(inString.length) - sb.append(inString.substring(0, index)) - } - return sb + return stringBuilder.toString() } - } \ No newline at end of file diff --git a/scarlet-protocol-stomp/src/test/java/com/tinder/scarlet/stomp/support/StompMessageEncoderTest.kt b/scarlet-protocol-stomp/src/test/java/com/tinder/scarlet/stomp/support/StompMessageEncoderTest.kt index 77503ef8..c3a2362b 100644 --- a/scarlet-protocol-stomp/src/test/java/com/tinder/scarlet/stomp/support/StompMessageEncoderTest.kt +++ b/scarlet-protocol-stomp/src/test/java/com/tinder/scarlet/stomp/support/StompMessageEncoderTest.kt @@ -1,33 +1,93 @@ package com.tinder.scarlet.stomp.support -import com.tinder.scarlet.stomp.core.StompCommand -import com.tinder.scarlet.stomp.core.StompMessage +import com.tinder.scarlet.stomp.core.models.StompCommand +import com.tinder.scarlet.stomp.core.models.StompMessage +import junit.framework.Assert.assertEquals +import junit.framework.Assert.assertTrue import org.junit.Test -class StompMessageEncoderDecoderTest { +class StompMessageEncoderTest { + + private val encoder = StompMessageEncoder() + + @Test + fun `encode frame with no headers and nobBody`() { + val frame = StompMessage.Builder() + .withPayload(ByteArray(0)) + .create(StompCommand.DISCONNECT) + + assertEquals("DISCONNECT\n\n\u0000", String(encoder.encode(frame))) + } + + @Test + fun `encode frame with headers`() { + val headers = StompHeaderAccessor.of().apply { + acceptVersion("1.2") + host("github.org") + }.createHeader() + + val frame = StompMessage.Builder() + .withPayload(ByteArray(0)) + .withHeaders(headers) + .create(StompCommand.CONNECT) + + val frameString = String(encoder.encode(frame)) + assertTrue( + "CONNECT\naccept-version:1.2\nhost:github.org\n\n\u0000" == frameString || + "CONNECT\nhost:github.org\naccept-version:1.2\n\n\u0000" == frameString + ) + } @Test - fun `correct_decode_message_after_encode`() { - val headers = StompHeaderAccessor.of( - mapOf( - "foo" to "bar", - "bar" to "foo" - ) - ).apply { destination("destination") } + fun `encode frame with headers that should be escaped`() { + val headers = StompHeaderAccessor.of() + .apply { putAll(mapOf("a:\r\n\\b" to "alpha:bravo\r\n\\")) } .createHeader() - val message = StompMessage.Builder() - .withPayload("foo") + val frame = StompMessage.Builder() + .withPayload(ByteArray(0)) .withHeaders(headers) - .create(StompCommand.SEND) + .create(StompCommand.DISCONNECT) - val data = StompMessageEncoder.encode(message) - val decodeMessage = StompMessageDecoder.decode(data) + assertEquals( + "DISCONNECT\na\\c\\r\\n\\\\b:alpha\\cbravo\\r\\n\\\\\n\n\u0000", + String(encoder.encode(frame)) + ) + } + + @Test + fun `encode frame with headers body`() { + val headers = StompHeaderAccessor.of().apply { + putAll(mapOf("a" to "alpha")) + destination("destination") + }.createHeader() - assert(decodeMessage.payload == "foo") - assert(decodeMessage.headers.destination == "destination") + val frame = StompMessage.Builder() + .withPayload("Message body") + .withHeaders(headers) + .create(StompCommand.SEND) - assert(decodeMessage.headers.size == 4)//because encoder add content-lenth + assertEquals( + "SEND\na:alpha\ndestination:destination\ncontent-length:12\n\nMessage body\u0000", + String(encoder.encode(frame)) + ) } + @Test + fun `encode frame with content length present`() { + val headers = StompHeaderAccessor.of().apply { + contentLength(22) + destination("destination") + }.createHeader() + + val frame = StompMessage.Builder() + .withPayload("Message body") + .withHeaders(headers) + .create(StompCommand.SEND) + + assertEquals( + "SEND\ndestination:destination\ncontent-length:12\n\nMessage body\u0000", + String(encoder.encode(frame)) + ) + } } \ No newline at end of file From bf65dd79db9e43cd3746476ee97483bf1f5758a1 Mon Sep 17 00:00:00 2001 From: NikitaZaltsman Date: Sat, 7 Mar 2020 15:56:08 +0700 Subject: [PATCH 22/32] add mock implementation of stomp decoder --- .../stomp/support/StompMessageDecoder.kt | 34 ++----------------- 1 file changed, 3 insertions(+), 31 deletions(-) diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompMessageDecoder.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompMessageDecoder.kt index 6504e129..2ece3879 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompMessageDecoder.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompMessageDecoder.kt @@ -2,40 +2,12 @@ package com.tinder.scarlet.stomp.support import com.tinder.scarlet.stomp.core.models.StompCommand import com.tinder.scarlet.stomp.core.models.StompMessage -import com.tinder.scarlet.stomp.support.StompMessageEncoder.TERMINATE_MESSAGE_SYMBOL -import java.io.StringReader -import java.util.Scanner import java.util.regex.Pattern -object StompMessageDecoder { +class StompMessageDecoder { private val PATTERN_HEADER = Pattern.compile("([^:\\s]+)\\s*:\\s*([^:\\s]+)") - fun decode(data: String): StompMessage = if (data.isNotEmpty()) { - val reader = Scanner(StringReader(data)) - reader.useDelimiter("\\n") - - val command = StompCommand.valueOf(reader.next()) - val headerAccessor = StompHeaderAccessor.of() - - while (reader.hasNext(PATTERN_HEADER)) { - val matcher = PATTERN_HEADER.matcher(reader.next()) - if (matcher.find()) { - headerAccessor[matcher.group(1)] = matcher.group(2) - } - } - - reader.skip("\n\n") - reader.useDelimiter(TERMINATE_MESSAGE_SYMBOL.toString()) - val payload = if (reader.hasNext()) reader.next() else null - - StompMessage.Builder() - .withPayload(payload) - .withHeaders(headerAccessor.createHeader()) - .create(command) - } else { - StompMessage.Builder() - .create(StompCommand.UNKNOWN) - } - + fun decode(data: ByteArray): StompMessage = StompMessage.Builder() + .create(StompCommand.UNKNOWN) } \ No newline at end of file From fbc9b19712c4051b1531470554011d4c58cbc2bc Mon Sep 17 00:00:00 2001 From: NikitaZaltsman Date: Sat, 7 Mar 2020 16:05:40 +0700 Subject: [PATCH 23/32] fix di config --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index c4a9a858..94ea03a6 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,7 +3,7 @@ version: 2 jobs: build: docker: - - image: circleci/android:api-28-alpha + - image: circleci/android:api-29 environment: TERM: dumb JAVA_TOOL_OPTIONS: "-Xmx1g" From b9c10d5b0da76ba7d18d3e1166bd4cc3723b1432 Mon Sep 17 00:00:00 2001 From: NikitaZaltsman Date: Sat, 7 Mar 2020 19:38:30 +0700 Subject: [PATCH 24/32] add documentation --- .../scarlet/stomp/core/StompDestination.kt | 8 +++++++ .../scarlet/stomp/core/StompMainChannel.kt | 20 ++---------------- .../scarlet/stomp/core/StompMessageChannel.kt | 5 ++++- .../tinder/scarlet/stomp/core/StompSender.kt | 13 ++++++++++-- .../scarlet/stomp/core/StompSubscriber.kt | 7 +++++-- .../scarlet/stomp/core/models/StompCommand.kt | 3 +++ .../scarlet/stomp/core/models/StompMessage.kt | 3 ++- .../scarlet/stomp/okhttp/OkHttpStompClient.kt | 4 ++++ .../stomp/okhttp/WebSocketConnection.kt | 21 +++++++++++++++++++ .../scarlet/stomp/okhttp/WebSocketFactory.kt | 3 +++ .../stomp/support/StompHeaderAccessor.kt | 6 ++++++ .../stomp/support/StompMessageDecoder.kt | 3 +++ .../stomp/support/StompMessageEncoder.kt | 8 +++++++ 13 files changed, 80 insertions(+), 24 deletions(-) diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompDestination.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompDestination.kt index b2ede48a..aba4d5e5 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompDestination.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompDestination.kt @@ -12,7 +12,15 @@ private typealias DestinationOpenRequestHeaderFactory = (channel: Channel) -> St private typealias MessageMetaDataFactory = (channel: Channel, message: Message) -> StompDestination.MessageMetaData /** + * Scarlet protocol implementation for create channel (OkHttpStompMessageChannel) for subscribe to + * queue by destination. + * @see StompMessageChannel * + * MessageMetaDataFactory is optional factory for create custom header for each message which will be sent + * by this StompMessageChannel. + * + * DestinationOpenRequestHeaderFactory is optional factory for create open request header which will be sent + * with subscribe message. */ class StompDestination( private val destination: String, diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompMainChannel.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompMainChannel.kt index e04825bc..3ed04d07 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompMainChannel.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompMainChannel.kt @@ -19,7 +19,8 @@ import java.util.concurrent.ConcurrentHashMap import kotlin.math.max /** - * + * The main channel, which is responsible for connecting and disconnecting to the stomp server. + * And also for sending messages and the logic of subscriptions. */ class StompMainChannel( private val configuration: Configuration, @@ -89,23 +90,6 @@ class StompMainChannel( return connection?.sendMessage(stompMessage) ?: false } - override fun convertAndSend( - payload: String, - destination: String, - headers: StompHeader? - ): Boolean { - val stompHeaders = StompHeaderAccessor.of(headers.orEmpty()) - .apply { destination(destination) } - .createHeader() - - val stompMessage = StompMessage.Builder() - .withPayload(payload) - .withHeaders(stompHeaders) - .create(StompCommand.SEND) - - return connection?.sendMessage(stompMessage) ?: false - } - override fun subscribe( destination: String, headers: StompHeader?, diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompMessageChannel.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompMessageChannel.kt index cb429f95..cbec36b5 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompMessageChannel.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompMessageChannel.kt @@ -5,6 +5,9 @@ import com.tinder.scarlet.Message import com.tinder.scarlet.MessageQueue import com.tinder.scarlet.Protocol +/** + * This channel is responsible for a specific subscription and its life cycle. + */ class StompMessageChannel( private val destination: String, private val stompSubscriber: StompSubscriber, @@ -49,7 +52,7 @@ class StompMessageChannel( ): Boolean = when (message) { is Message.Text -> { val metaData = messageMetaData as? StompDestination.MessageMetaData - stompSender.convertAndSend(message.value, destination, metaData?.headers) + stompSender.convertAndSend(message.value.toByteArray(), destination, metaData?.headers) } is Message.Bytes -> { val metaData = messageMetaData as? StompDestination.MessageMetaData diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompSender.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompSender.kt index d79fdcba..bf49087e 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompSender.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompSender.kt @@ -2,9 +2,18 @@ package com.tinder.scarlet.stomp.core import com.tinder.scarlet.stomp.core.models.StompHeader +/** + * Operations for sending messages to a destination. + */ interface StompSender { + /** + * Convert the given byte array to serialized form, possibly using a + * StompMessageEncoder, wrap it as a message and send it to a given destination. + * @param payload the byte array to use as payload + * @param destination the target destination + * @param headers headers for the message to send + * @see StompMessageEncoder + */ fun convertAndSend(payload: ByteArray, destination: String, headers: StompHeader?): Boolean - - fun convertAndSend(payload: String, destination: String, headers: StompHeader?): Boolean } \ No newline at end of file diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompSubscriber.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompSubscriber.kt index 63405109..785b1615 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompSubscriber.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompSubscriber.kt @@ -5,15 +5,18 @@ import com.tinder.scarlet.stomp.core.models.StompMessage typealias StompListener = (StompMessage) -> Unit +/** + * Interface use for subscribe and unsubscribe to STOMP queue. + */ interface StompSubscriber { /** - * + * Subscribe to given destination with headers. */ fun subscribe(destination: String, headers: StompHeader?, listener: StompListener) /** - * + * Unsubscribe from given destination. */ fun unsubscribe(destination: String) } \ No newline at end of file diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/models/StompCommand.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/models/StompCommand.kt index c43c1715..6379f0be 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/models/StompCommand.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/models/StompCommand.kt @@ -1,5 +1,8 @@ package com.tinder.scarlet.stomp.core.models +/** + * Represents a STOMP command. + */ enum class StompCommand( val isBodyAllowed: Boolean = false, val isDestinationRequired: Boolean = false diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/models/StompMessage.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/models/StompMessage.kt index dbbb13bd..9044b53c 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/models/StompMessage.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/models/StompMessage.kt @@ -3,7 +3,8 @@ package com.tinder.scarlet.stomp.core.models import com.tinder.scarlet.stomp.support.StompHeaderAccessor /** - * + * Stomp message representation with headers, payload and command. + * @see StompCommand */ class StompMessage private constructor( val command: StompCommand, diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompClient.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompClient.kt index 06d54ebf..7441f6e3 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompClient.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompClient.kt @@ -11,6 +11,10 @@ import okhttp3.WebSocketListener private typealias ClientOpenRequestHeaderFactory = (channel: Channel) -> OkHttpStompClient.ClientOpenRequest +/** + * Scarlet protocol implementation for create StompMainChannel + * @see StompMainChannel + */ class OkHttpStompClient( private val configuration: StompMainChannel.Configuration, private val okHttpClient: OkHttpClient, diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/WebSocketConnection.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/WebSocketConnection.kt index 292401ae..ba87e273 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/WebSocketConnection.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/WebSocketConnection.kt @@ -9,6 +9,9 @@ import okio.ByteString import java.util.concurrent.Executors import java.util.concurrent.TimeUnit +/** + * Okhttp websocket based implementation of {@link Connection}. + */ class WebSocketConnection( private val webSocket: WebSocket ) : Connection, MessageHandler { @@ -30,6 +33,9 @@ class WebSocketConnection( private const val NORMAL_CLOSURE_REASON = "Normal closure" } + /** + * {@inheritDoc} + */ override fun sendMessage(message: StompMessage): Boolean { val lastWriteTime = lastWriteTime if (lastWriteTime != -1L) { @@ -40,6 +46,9 @@ class WebSocketConnection( return webSocket.send(byteString) } + /** + * {@inheritDoc} + */ override fun onReceiveInactivity(duration: Long, runnable: () -> Unit) { lastReadTime = System.currentTimeMillis() executor.scheduleWithFixedDelay({ @@ -49,6 +58,9 @@ class WebSocketConnection( }, 0, duration / 2, TimeUnit.MILLISECONDS) } + /** + * {@inheritDoc} + */ override fun onWriteInactivity(duration: Long, runnable: () -> Unit) { lastWriteTime = System.currentTimeMillis() executor.scheduleWithFixedDelay({ @@ -58,16 +70,25 @@ class WebSocketConnection( }, 0, duration / 2, TimeUnit.MILLISECONDS) } + /** + * {@inheritDoc} + */ override fun forceClose() { webSocket.cancel() executor.shutdown() } + /** + * {@inheritDoc} + */ override fun close() { webSocket.close(NORMAL_CLOSURE_STATUS_CODE, NORMAL_CLOSURE_REASON) executor.shutdown() } + /** + * {@inheritDoc} + */ override fun handle(data: ByteArray): StompMessage { val lastReadTime = lastReadTime if (lastReadTime != -1L) { diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/WebSocketFactory.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/WebSocketFactory.kt index 4c4a776c..65d1df2b 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/WebSocketFactory.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/WebSocketFactory.kt @@ -6,6 +6,9 @@ package com.tinder.scarlet.stomp.okhttp import okhttp3.Request import okhttp3.WebSocketListener +/** + * An factory for create STOMP WebSocket connection. + */ interface WebSocketFactory { fun createWebSocket(request: Request, listener: WebSocketListener) diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompHeaderAccessor.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompHeaderAccessor.kt index fabc1360..1a2e3d76 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompHeaderAccessor.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompHeaderAccessor.kt @@ -2,6 +2,12 @@ package com.tinder.scarlet.stomp.support import com.tinder.scarlet.stomp.core.models.StompHeader +/** + * A base for classes providing strongly typed getters and setters as well as + * behavior around specific categories of headers (e.g. STOMP headers). + * Supports creating new headers, modifying existing headers, + * or copying and modifying existing headers. + */ class StompHeaderAccessor private constructor(headers: Map) { companion object { diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompMessageDecoder.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompMessageDecoder.kt index 2ece3879..a9b89923 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompMessageDecoder.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompMessageDecoder.kt @@ -4,6 +4,9 @@ import com.tinder.scarlet.stomp.core.models.StompCommand import com.tinder.scarlet.stomp.core.models.StompMessage import java.util.regex.Pattern +/** + * An decoder for STOMP frames. + */ class StompMessageDecoder { private val PATTERN_HEADER = Pattern.compile("([^:\\s]+)\\s*:\\s*([^:\\s]+)") diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompMessageEncoder.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompMessageEncoder.kt index 9746b797..3cec18a4 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompMessageEncoder.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompMessageEncoder.kt @@ -6,6 +6,9 @@ import com.tinder.scarlet.stomp.core.models.StompMessage import java.io.ByteArrayOutputStream import java.io.DataOutputStream +/** + * An encoder for STOMP frames. + */ class StompMessageEncoder { companion object { @@ -17,6 +20,11 @@ class StompMessageEncoder { private const val START_HEADERS_SIZE = 64 } + /** + * Encodes the given STOMP {@code stompMessage} into a byte array. + * @param stompMessage the message to encode + * @return the encoded message + */ fun encode(stompMessage: StompMessage): ByteArray { val arraySize = START_HEADERS_SIZE + stompMessage.payload.size val arrayOutputStream = ByteArrayOutputStream(arraySize) From d027a510b47149fca28c1db9127cb73fabdc0ad1 Mon Sep 17 00:00:00 2001 From: NikitaZaltsman Date: Sat, 7 Mar 2020 19:38:45 +0700 Subject: [PATCH 25/32] fix test package name --- .../scarlet/stomp/StompIntegrationTest.kt | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) rename scarlet-protocol-stomp/src/test/java/com/{tindre => tinder}/scarlet/stomp/StompIntegrationTest.kt (93%) diff --git a/scarlet-protocol-stomp/src/test/java/com/tindre/scarlet/stomp/StompIntegrationTest.kt b/scarlet-protocol-stomp/src/test/java/com/tinder/scarlet/stomp/StompIntegrationTest.kt similarity index 93% rename from scarlet-protocol-stomp/src/test/java/com/tindre/scarlet/stomp/StompIntegrationTest.kt rename to scarlet-protocol-stomp/src/test/java/com/tinder/scarlet/stomp/StompIntegrationTest.kt index 9bcaa347..1d3ef4eb 100644 --- a/scarlet-protocol-stomp/src/test/java/com/tindre/scarlet/stomp/StompIntegrationTest.kt +++ b/scarlet-protocol-stomp/src/test/java/com/tinder/scarlet/stomp/StompIntegrationTest.kt @@ -2,7 +2,7 @@ * © 2018 Match Group, LLC. */ -package com.tindre.scarlet.stomp +package com.tinder.scarlet.stomp import com.tinder.scarlet.ProtocolEvent import com.tinder.scarlet.Stream @@ -64,8 +64,14 @@ class StompIntegrationTest { @Test fun test2() { val connection1 = StompConnection() - connection1.open(HOST, PORT) - connection1.connect(LOGIN, PASSWORD) + connection1.open( + HOST, + PORT + ) + connection1.connect( + LOGIN, + PASSWORD + ) connection1.begin("tx1") connection1.send(DESTINATION, "message1", "tx1", null) connection1.send(DESTINATION, "message2", "tx1", null) From ffa9805fe044d0da88b9a81bb44509c963585dd2 Mon Sep 17 00:00:00 2001 From: NikitaZaltsman Date: Sun, 8 Mar 2020 19:16:33 +0700 Subject: [PATCH 26/32] add message decoder implementation --- .../tinder/scarlet/stomp/core/StompSender.kt | 2 +- .../stomp/support/StompHeaderAccessor.kt | 14 +- .../stomp/support/StompMessageDecoder.kt | 165 +++++++++++++++++- 3 files changed, 171 insertions(+), 10 deletions(-) diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompSender.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompSender.kt index bf49087e..fde977a8 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompSender.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompSender.kt @@ -11,7 +11,7 @@ interface StompSender { * Convert the given byte array to serialized form, possibly using a * StompMessageEncoder, wrap it as a message and send it to a given destination. * @param payload the byte array to use as payload - * @param destination the target destination + * @param destination the target destination * @param headers headers for the message to send * @see StompMessageEncoder */ diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompHeaderAccessor.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompHeaderAccessor.kt index 1a2e3d76..a55d5d22 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompHeaderAccessor.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompHeaderAccessor.kt @@ -29,6 +29,16 @@ class StompHeaderAccessor private constructor(headers: Map) { mutableHeaders[key] = value } + var contentLength: Int? + get() = try { + mutableHeaders[StompHeader.CONTENT_LENGTH]?.toInt() + } catch (ex: NumberFormatException) { + null + } + set(value) { + mutableHeaders[StompHeader.CONTENT_LENGTH] = value.toString() + } + fun heartBeat(sendInterval: Long, receiveInterval: Long) { mutableHeaders[StompHeader.HEARTBEAT] = "$sendInterval,$receiveInterval" } @@ -53,10 +63,6 @@ class StompHeaderAccessor private constructor(headers: Map) { mutableHeaders[StompHeader.CONTENT_TYPE] = contentType } - fun contentLength(length: Int) { - mutableHeaders[StompHeader.CONTENT_LENGTH] = length.toString() - } - fun host(host: String) { mutableHeaders[StompHeader.HOST] = host } diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompMessageDecoder.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompMessageDecoder.kt index a9b89923..d548b981 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompMessageDecoder.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompMessageDecoder.kt @@ -2,15 +2,170 @@ package com.tinder.scarlet.stomp.support import com.tinder.scarlet.stomp.core.models.StompCommand import com.tinder.scarlet.stomp.core.models.StompMessage -import java.util.regex.Pattern +import java.io.ByteArrayOutputStream +import java.nio.ByteBuffer /** * An decoder for STOMP frames. */ class StompMessageDecoder { - private val PATTERN_HEADER = Pattern.compile("([^:\\s]+)\\s*:\\s*([^:\\s]+)") + fun decode(data: ByteArray): StompMessage { + val byteBuffer = ByteBuffer.wrap(data) + return decode(byteBuffer) + } - fun decode(data: ByteArray): StompMessage = StompMessage.Builder() - .create(StompCommand.UNKNOWN) -} \ No newline at end of file + private fun decode(byteBuffer: ByteBuffer): StompMessage { + skipLeadingEol(byteBuffer) + val command = readCommand(byteBuffer) + + return if (command.isNotEmpty()) { + + val stompCommand = StompCommand.valueOf(command) + val headerAccessor = StompHeaderAccessor.of() + + val payload: ByteArray? = null + if (byteBuffer.isNotEmpty()) { + readHeaders(byteBuffer, headerAccessor) + val payload = readPayload(byteBuffer, headerAccessor) + + if (payload != null) { + if (payload.isNotEmpty()) { + if (!stompCommand.isBodyAllowed) { + throw IllegalStateException(stompCommand.toString() + " shouldn't have a payload: length=" + payload.size + ", headers=" + headerAccessor) + } + } + } else { + byteBuffer.reset() + } + } + StompMessage.Builder() + .withPayload(payload ?: ByteArray(0)) + .create(stompCommand) + } else { + StompMessage.Builder().create(StompCommand.UNKNOWN) + } + } + + private fun skipLeadingEol(byteBuffer: ByteBuffer) { + while (true) { + if (!tryConsumeEndOfLine(byteBuffer)) break + } + } + + private fun readPayload( + byteBuffer: ByteBuffer, + headerAccessor: StompHeaderAccessor + ): ByteArray? { + val contentLength = headerAccessor.contentLength + if (contentLength != null && contentLength >= 0) { + return if (byteBuffer.remaining() > contentLength) { + val payload = ByteArray(contentLength) + byteBuffer[payload] + check( + byteBuffer.get().toInt() == 0 + ) { "Frame must be terminated with a null octet" } + payload + } else { + null + } + } else { + val payload = ByteArrayOutputStream(256) + while (byteBuffer.remaining() > 0) { + val b = byteBuffer.get() + if (b.toInt() == 0) { + return payload.toByteArray() + } else { + payload.write(b.toInt()) + } + } + } + return null + } + + private fun readHeaders(byteBuffer: ByteBuffer, headerAccessor: StompHeaderAccessor) { + while (true) { + val headerStream = ByteArrayOutputStream(256) + var headerComplete = false + while (byteBuffer.hasRemaining()) { + if (tryConsumeEndOfLine(byteBuffer)) { + headerComplete = true + break + } + headerStream.write(byteBuffer.get().toInt()) + } + if (headerStream.size() > 0 && headerComplete) { + val header = String(headerStream.toByteArray(), Charsets.UTF_8) + val colonIndex = header.indexOf(':') + if (colonIndex <= 0) { + if (byteBuffer.isNotEmpty()) throw IllegalStateException("Illegal header: '$header'. A header must be of the form :[].") + } else { + val headerName = unescape(header.substring(0, colonIndex)) + val headerValue = unescape(header.substring(colonIndex + 1)) + headerAccessor[headerName] = headerValue + } + } else { + break + } + } + } + + private fun readCommand(byteBuffer: ByteBuffer): String { + val command = ByteArrayOutputStream(256) + while (byteBuffer.isNotEmpty() && !tryConsumeEndOfLine(byteBuffer)) { + command.write(byteBuffer.get().toInt()) + } + return String(command.toByteArray(), Charsets.UTF_8) + } + + /** + * Try to read an EOL incrementing the buffer position if successful. + * @return whether an EOL was consumed + */ + private fun tryConsumeEndOfLine(byteBuffer: ByteBuffer): Boolean = byteBuffer + .takeIf { buffer -> buffer.isNotEmpty() } + ?.let { buffer -> + when (byteBuffer.get()) { + '\n'.toByte() -> true + '\r'.toByte() -> checkSequence(byteBuffer) + else -> { + buffer.position(buffer.position() - 1) + false + } + } + } ?: false + + /** + * See STOMP Spec 1.2: + * ["Value Encoding"](https://stomp.github.io/stomp-specification-1.2.html#Value_Encoding). + */ + private fun unescape(inString: String): String { + val stringBuilder = StringBuilder(inString.length) + var pos = 0 // position in the old string + var index = inString.indexOf('\\') + while (index >= 0) { + stringBuilder.append(inString.substring(pos, index)) + check(index + 1 < inString.length) { "Illegal escape sequence at index $index: $inString" } + + when (inString[index + 1]) { + 'r' -> stringBuilder.append('\r') + 'n' -> stringBuilder.append('\n') + 'c' -> stringBuilder.append(':') + '\\' -> stringBuilder.append('\\') + else -> throw IllegalStateException("Illegal escape sequence at index $index: $inString") // should never happen + } + pos = index + 2 + index = inString.indexOf('\\', pos) + } + stringBuilder.append(inString.substring(pos)) + return stringBuilder.toString() + } + + private fun checkSequence(byteBuffer: ByteBuffer): Boolean { + val nextChartIsNewLine = byteBuffer.get() == '\n'.toByte() + check(byteBuffer.remaining() > 0 && nextChartIsNewLine) { "'\\r' must be followed by '\\n'" } + return true + } + + private fun ByteBuffer.isNotEmpty(): Boolean = remaining() > 0 +} From 680f8f5950eb1ee864b0b1b7504f5ce6a303927a Mon Sep 17 00:00:00 2001 From: NikitaZaltsman Date: Mon, 9 Mar 2020 00:24:49 +0700 Subject: [PATCH 27/32] Add id generator interface refactoring stomp header accessor refactoring Change unknown stomp command type to heartbeat --- .../tinder/scarlet/stomp/core/IdGenerator.kt | 6 ++ .../scarlet/stomp/core/StompDestination.kt | 4 +- .../scarlet/stomp/core/StompMainChannel.kt | 30 ++++---- .../scarlet/stomp/core/models/StompCommand.kt | 2 +- .../scarlet/stomp/okhttp/OkHttpStompClient.kt | 11 +-- .../stomp/support/StompHeaderAccessor.kt | 70 +++++++++++++------ .../stomp/support/StompMessageEncoder.kt | 2 +- .../stomp/support/StompMessageEncoderTest.kt | 10 +-- 8 files changed, 86 insertions(+), 49 deletions(-) create mode 100644 scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/IdGenerator.kt diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/IdGenerator.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/IdGenerator.kt new file mode 100644 index 00000000..fe04ca6a --- /dev/null +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/IdGenerator.kt @@ -0,0 +1,6 @@ +package com.tinder.scarlet.stomp.core + +interface IdGenerator { + + fun generateId(): String +} \ No newline at end of file diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompDestination.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompDestination.kt index aba4d5e5..1684407c 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompDestination.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompDestination.kt @@ -8,8 +8,8 @@ import com.tinder.scarlet.stomp.core.models.StompHeader import com.tinder.scarlet.utils.SimpleChannelFactory import com.tinder.scarlet.utils.SimpleProtocolOpenRequestFactory -private typealias DestinationOpenRequestHeaderFactory = (channel: Channel) -> StompDestination.DestinationOpenRequest -private typealias MessageMetaDataFactory = (channel: Channel, message: Message) -> StompDestination.MessageMetaData +private typealias DestinationOpenRequestHeaderFactory = (Channel) -> StompDestination.DestinationOpenRequest +private typealias MessageMetaDataFactory = (Channel, Message) -> StompDestination.MessageMetaData /** * Scarlet protocol implementation for create channel (OkHttpStompMessageChannel) for subscribe to diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompMainChannel.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompMainChannel.kt index 3ed04d07..a42b9ffb 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompMainChannel.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompMainChannel.kt @@ -14,7 +14,6 @@ import okhttp3.Response import okhttp3.WebSocket import okhttp3.WebSocketListener import okio.ByteString -import java.util.UUID import java.util.concurrent.ConcurrentHashMap import kotlin.math.max @@ -24,6 +23,7 @@ import kotlin.math.max */ class StompMainChannel( private val configuration: Configuration, + private val idGenerator: IdGenerator, private val webSocketFactory: WebSocketFactory, private val listener: Channel.Listener ) : Channel, StompSender, StompSubscriber { @@ -79,7 +79,7 @@ class StompMainChannel( headers: StompHeader? ): Boolean { val stompHeaders = StompHeaderAccessor.of(headers.orEmpty()) - .apply { destination(destination) } + .apply { this.destination = destination } .createHeader() val stompMessage = StompMessage.Builder() @@ -97,11 +97,11 @@ class StompMainChannel( ) { check(!topicIds.containsKey(destination)) { "Already has subscription to destination=$destination" } check(!subscriptions.containsKey(destination)) { "Already has subscription to destination=$destination" } - val generateId = UUID.randomUUID().toString() + val generateId = idGenerator.generateId() val stompHeaders = StompHeaderAccessor.of(headers.orEmpty()) .apply { - subscriptionId(generateId) - destination(destination) + this.subscriptionId = generateId + this.destination = destination } .createHeader() @@ -120,7 +120,7 @@ class StompMainChannel( ?: throw IllegalStateException("Unknown destination=$destination") val stompHeaders = StompHeaderAccessor.of() - .apply { subscriptionId(subscriptionId) } + .apply { this.subscriptionId = subscriptionId } .createHeader() val stompMessage = StompMessage.Builder() @@ -168,7 +168,7 @@ class StompMainChannel( private fun sendHeartBeat() { val stompMessage = StompMessage.Builder() - .create(StompCommand.UNKNOWN) + .create(StompCommand.HEARTBEAT) connection?.sendMessage(stompMessage) } @@ -227,10 +227,10 @@ class StompMainChannel( private fun sendConnectMessage(host: String, login: String? = null, passcode: String? = null) { val stompHeaderAccessor = StompHeaderAccessor.of() .apply { - host(host) - acceptVersion(ACCEPT_VERSION) - login?.let(::login) - passcode?.let(::passcode) + this.host = host + this.acceptVersion = ACCEPT_VERSION + this.login = login + this.passcode = passcode } val clientSendInterval = configuration.heartbeatSendInterval @@ -261,6 +261,7 @@ class StompMainChannel( ) class Factory( + private val idGenerator: IdGenerator, private val configuration: Configuration, private val webSocketFactory: WebSocketFactory ) : Channel.Factory { @@ -270,9 +271,10 @@ class StompMainChannel( parent: Channel? ): Channel? { return StompMainChannel( - configuration, - webSocketFactory, - listener + configuration = configuration, + idGenerator = idGenerator, + webSocketFactory = webSocketFactory, + listener = listener ) } } diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/models/StompCommand.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/models/StompCommand.kt index 6379f0be..2d6758a4 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/models/StompCommand.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/models/StompCommand.kt @@ -20,5 +20,5 @@ enum class StompCommand( ERROR(isBodyAllowed = true), // heartbeat - UNKNOWN + HEARTBEAT } \ No newline at end of file diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompClient.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompClient.kt index 7441f6e3..944bb815 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompClient.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompClient.kt @@ -3,14 +3,13 @@ package com.tinder.scarlet.stomp.okhttp import com.tinder.scarlet.Channel import com.tinder.scarlet.Protocol import com.tinder.scarlet.ProtocolSpecificEventAdapter +import com.tinder.scarlet.stomp.core.IdGenerator import com.tinder.scarlet.stomp.core.StompMainChannel import com.tinder.scarlet.utils.SimpleProtocolOpenRequestFactory import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.WebSocketListener -private typealias ClientOpenRequestHeaderFactory = (channel: Channel) -> OkHttpStompClient.ClientOpenRequest - /** * Scarlet protocol implementation for create StompMainChannel * @see StompMainChannel @@ -18,12 +17,14 @@ private typealias ClientOpenRequestHeaderFactory = (channel: Channel) -> OkHttpS class OkHttpStompClient( private val configuration: StompMainChannel.Configuration, private val okHttpClient: OkHttpClient, - private val requestFactory: ClientOpenRequestHeaderFactory + private val requestFactory: (Channel) -> ClientOpenRequest, + private val idGenerator: IdGenerator ) : Protocol { override fun createChannelFactory() = StompMainChannel.Factory( - configuration, - object : WebSocketFactory { + idGenerator = idGenerator, + configuration = configuration, + webSocketFactory = object : WebSocketFactory { override fun createWebSocket(request: Request, listener: WebSocketListener) { okHttpClient.newWebSocket(request, listener) } diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompHeaderAccessor.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompHeaderAccessor.kt index a55d5d22..c31d2925 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompHeaderAccessor.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompHeaderAccessor.kt @@ -47,33 +47,61 @@ class StompHeaderAccessor private constructor(headers: Map) { mutableHeaders.putAll(headers) } - fun subscriptionId(subscriptionId: String) { - mutableHeaders[StompHeader.ID] = subscriptionId - } + var subscriptionId: String? + get() = mutableHeaders[StompHeader.ID] + set(value) { + if (value != null) { + mutableHeaders[StompHeader.ID] = value + } + } - fun destination(destination: String) { - mutableHeaders[StompHeader.DESTINATION] = destination - } + var destination: String? + get() = mutableHeaders[StompHeader.DESTINATION] + set(value) { + if (value != null) { + mutableHeaders[StompHeader.DESTINATION] = value + } + } - fun acceptVersion(acceptVersion: String) { - mutableHeaders[StompHeader.ACCEPT_VERSION] = acceptVersion - } + var acceptVersion: String? + get() = mutableHeaders[StompHeader.ACCEPT_VERSION] + set(value) { + if (value != null) { + mutableHeaders[StompHeader.ACCEPT_VERSION] = value + } + } - fun contentType(contentType: String) { - mutableHeaders[StompHeader.CONTENT_TYPE] = contentType - } + var contentType: String? + get() = mutableHeaders[StompHeader.CONTENT_TYPE] + set(value) { + if (value != null) { + mutableHeaders[StompHeader.CONTENT_TYPE] = value + } + } - fun host(host: String) { - mutableHeaders[StompHeader.HOST] = host - } + var host: String? + get() = mutableHeaders[StompHeader.HOST] + set(value) { + if (value != null) { + mutableHeaders[StompHeader.HOST] = value + } + } - fun login(login: String) { - mutableHeaders[StompHeader.LOGIN] = login - } + var login: String? + get() = mutableHeaders[StompHeader.LOGIN] + set(value) { + if (value != null) { + mutableHeaders[StompHeader.LOGIN] = value + } + } - fun passcode(passcode: String) { - mutableHeaders[StompHeader.PASSCODE] = passcode - } + var passcode: String? + get() = mutableHeaders[StompHeader.PASSCODE] + set(value) { + if (value != null) { + mutableHeaders[StompHeader.PASSCODE] = value + } + } fun message(message: String) { mutableHeaders[STOMP_MESSAGE_HEADER] = message diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompMessageEncoder.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompMessageEncoder.kt index 3cec18a4..876abc1d 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompMessageEncoder.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompMessageEncoder.kt @@ -32,7 +32,7 @@ class StompMessageEncoder { DataOutputStream(arrayOutputStream).use { dataOutputStream -> val command = stompMessage.command - if (command != StompCommand.UNKNOWN) { + if (command != StompCommand.HEARTBEAT) { writeMessage(dataOutputStream, command, stompMessage) } else { dataOutputStream.writeChars(HEARTBEAT_PAYLOAD) diff --git a/scarlet-protocol-stomp/src/test/java/com/tinder/scarlet/stomp/support/StompMessageEncoderTest.kt b/scarlet-protocol-stomp/src/test/java/com/tinder/scarlet/stomp/support/StompMessageEncoderTest.kt index c3a2362b..8aee2888 100644 --- a/scarlet-protocol-stomp/src/test/java/com/tinder/scarlet/stomp/support/StompMessageEncoderTest.kt +++ b/scarlet-protocol-stomp/src/test/java/com/tinder/scarlet/stomp/support/StompMessageEncoderTest.kt @@ -22,8 +22,8 @@ class StompMessageEncoderTest { @Test fun `encode frame with headers`() { val headers = StompHeaderAccessor.of().apply { - acceptVersion("1.2") - host("github.org") + acceptVersion = "1.2" + host = "github.org" }.createHeader() val frame = StompMessage.Builder() @@ -59,7 +59,7 @@ class StompMessageEncoderTest { fun `encode frame with headers body`() { val headers = StompHeaderAccessor.of().apply { putAll(mapOf("a" to "alpha")) - destination("destination") + destination = "destination" }.createHeader() val frame = StompMessage.Builder() @@ -76,8 +76,8 @@ class StompMessageEncoderTest { @Test fun `encode frame with content length present`() { val headers = StompHeaderAccessor.of().apply { - contentLength(22) - destination("destination") + contentLength = 22 + destination = "destination" }.createHeader() val frame = StompMessage.Builder() From 9b7dc5a45e78177680b70d62eb4df510f87dbbfc Mon Sep 17 00:00:00 2001 From: NikitaZaltsman Date: Mon, 9 Mar 2020 22:25:11 +0700 Subject: [PATCH 28/32] refactoring stomp header accessor Make method handle result as nullable add java doc to IdGenerator Refactoring stomp message decoder and add tests add copyright refactoring packages remove extra update test deps fix crash fix bug with sending bytes --- dependencies.gradle | 9 +- .../tinder/scarlet/stomp/core/IdGenerator.kt | 6 - .../scarlet/stomp/okhttp/MessageHandler.kt | 11 - .../okhttp/{ => client}/OkHttpStompClient.kt | 18 +- .../client/OkHttpStompDestination.kt} | 19 +- .../client/OkHttpStompMainChannel.kt} | 55 ++--- .../client/OkHttpStompMessageChannel.kt} | 21 +- .../{ => client}/WebSocketConnection.kt | 20 +- .../stomp/{ => okhttp}/core/Connection.kt | 7 +- .../scarlet/stomp/okhttp/core/IdGenerator.kt | 12 ++ .../stomp/okhttp/core/MessageHandler.kt | 14 ++ .../stomp/{ => okhttp}/core/StompSender.kt | 7 +- .../{ => okhttp}/core/StompSubscriber.kt | 9 +- .../okhttp/{ => core}/WebSocketFactory.kt | 2 +- .../stomp/okhttp/generator/UuidGenerator.kt | 9 + .../{core => okhttp}/models/StompCommand.kt | 5 +- .../{core => okhttp}/models/StompHeader.kt | 2 +- .../{core => okhttp}/models/StompMessage.kt | 7 +- .../support/StompHeaderAccessor.kt | 39 ++-- .../support/StompMessageDecoder.kt | 125 ++++++----- .../support/StompMessageEncoder.kt | 11 +- .../stomp/support/StompMessageDecoderTest.kt | 197 ++++++++++++++++++ .../stomp/support/StompMessageEncoderTest.kt | 13 +- 23 files changed, 459 insertions(+), 159 deletions(-) delete mode 100644 scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/IdGenerator.kt delete mode 100644 scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/MessageHandler.kt rename scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/{ => client}/OkHttpStompClient.kt (72%) rename scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/{core/StompDestination.kt => okhttp/client/OkHttpStompDestination.kt} (83%) rename scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/{core/StompMainChannel.kt => okhttp/client/OkHttpStompMainChannel.kt} (83%) rename scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/{core/StompMessageChannel.kt => okhttp/client/OkHttpStompMessageChannel.kt} (71%) rename scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/{ => client}/WebSocketConnection.kt (82%) rename scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/{ => okhttp}/core/Connection.kt (86%) create mode 100644 scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/core/IdGenerator.kt create mode 100644 scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/core/MessageHandler.kt rename scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/{ => okhttp}/core/StompSender.kt (79%) rename scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/{ => okhttp}/core/StompSubscriber.kt (67%) rename scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/{ => core}/WebSocketFactory.kt (85%) create mode 100644 scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/generator/UuidGenerator.kt rename scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/{core => okhttp}/models/StompCommand.kt (85%) rename scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/{core => okhttp}/models/StompHeader.kt (98%) rename scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/{core => okhttp}/models/StompMessage.kt (92%) rename scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/{ => okhttp}/support/StompHeaderAccessor.kt (79%) rename scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/{ => okhttp}/support/StompMessageDecoder.kt (59%) rename scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/{ => okhttp}/support/StompMessageEncoder.kt (93%) create mode 100644 scarlet-protocol-stomp/src/test/java/com/tinder/scarlet/stomp/support/StompMessageDecoderTest.kt diff --git a/dependencies.gradle b/dependencies.gradle index 2647406e..171a7a9a 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -62,10 +62,11 @@ ext { socketIoClient = 'io.socket:socket.io-client:1.0.0' nettySocketIoServer = 'com.corundumstudio.socketio:netty-socketio:1.7.12' pahoMqtt = 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.0' - activeMqJUnit = 'org.apache.activemq.tooling:activemq-junit:5.13.1' - activeMqBroker = 'org.apache.activemq:activemq-broker:5.15.6' - activeMqStomp = 'org.apache.activemq:activemq-stomp:5.15.6' - activeMqMqtt = 'org.apache.activemq:activemq-mqtt:5.15.6' + activeMqJUnit = 'org.apache.activemq.tooling:activemq-junit:5.15.11' + activeMqBroker = 'org.apache.activemq:activemq-broker:5.15.11' + activeMqStomp = 'org.apache.activemq:activemq-stomp:5.15.11' + activeMqMqtt = 'org.apache.activemq:activemq-mqtt:5.15.11' + activeHttp = 'org.apache.activemq:activemq-http:5.15.11' slf4jSimple = 'org.slf4j:slf4j-simple:1.7.25' diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/IdGenerator.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/IdGenerator.kt deleted file mode 100644 index fe04ca6a..00000000 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/IdGenerator.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.tinder.scarlet.stomp.core - -interface IdGenerator { - - fun generateId(): String -} \ No newline at end of file diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/MessageHandler.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/MessageHandler.kt deleted file mode 100644 index 08b67446..00000000 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/MessageHandler.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.tinder.scarlet.stomp.okhttp - -import com.tinder.scarlet.stomp.core.models.StompMessage - -interface MessageHandler { - - /** - * Convert given raw data byte array to stomp message - */ - fun handle(data: ByteArray): StompMessage -} \ No newline at end of file diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompClient.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/client/OkHttpStompClient.kt similarity index 72% rename from scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompClient.kt rename to scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/client/OkHttpStompClient.kt index 944bb815..3bb28fbc 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompClient.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/client/OkHttpStompClient.kt @@ -1,10 +1,14 @@ -package com.tinder.scarlet.stomp.okhttp +/* + * © 2018 Match Group, LLC. + */ +package com.tinder.scarlet.stomp.okhttp.client import com.tinder.scarlet.Channel import com.tinder.scarlet.Protocol import com.tinder.scarlet.ProtocolSpecificEventAdapter -import com.tinder.scarlet.stomp.core.IdGenerator -import com.tinder.scarlet.stomp.core.StompMainChannel +import com.tinder.scarlet.stomp.okhttp.core.IdGenerator +import com.tinder.scarlet.stomp.okhttp.core.WebSocketFactory +import com.tinder.scarlet.stomp.okhttp.generator.UuidGenerator import com.tinder.scarlet.utils.SimpleProtocolOpenRequestFactory import okhttp3.OkHttpClient import okhttp3.Request @@ -12,16 +16,16 @@ import okhttp3.WebSocketListener /** * Scarlet protocol implementation for create StompMainChannel - * @see StompMainChannel + * @see OkHttpStompMainChannel */ class OkHttpStompClient( - private val configuration: StompMainChannel.Configuration, + private val configuration: OkHttpStompMainChannel.Configuration, private val okHttpClient: OkHttpClient, private val requestFactory: (Channel) -> ClientOpenRequest, - private val idGenerator: IdGenerator + private val idGenerator: IdGenerator = UuidGenerator() ) : Protocol { - override fun createChannelFactory() = StompMainChannel.Factory( + override fun createChannelFactory() = OkHttpStompMainChannel.Factory( idGenerator = idGenerator, configuration = configuration, webSocketFactory = object : WebSocketFactory { diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompDestination.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/client/OkHttpStompDestination.kt similarity index 83% rename from scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompDestination.kt rename to scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/client/OkHttpStompDestination.kt index 1684407c..ad91b263 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompDestination.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/client/OkHttpStompDestination.kt @@ -1,20 +1,23 @@ -package com.tinder.scarlet.stomp.core +/* + * © 2018 Match Group, LLC. + */ +package com.tinder.scarlet.stomp.okhttp.client import com.tinder.scarlet.Channel import com.tinder.scarlet.Message import com.tinder.scarlet.Protocol import com.tinder.scarlet.ProtocolSpecificEventAdapter -import com.tinder.scarlet.stomp.core.models.StompHeader +import com.tinder.scarlet.stomp.okhttp.models.StompHeader import com.tinder.scarlet.utils.SimpleChannelFactory import com.tinder.scarlet.utils.SimpleProtocolOpenRequestFactory -private typealias DestinationOpenRequestHeaderFactory = (Channel) -> StompDestination.DestinationOpenRequest -private typealias MessageMetaDataFactory = (Channel, Message) -> StompDestination.MessageMetaData +private typealias DestinationOpenRequestHeaderFactory = (Channel) -> OkHttpStompDestination.DestinationOpenRequest +private typealias MessageMetaDataFactory = (Channel, Message) -> OkHttpStompDestination.MessageMetaData /** * Scarlet protocol implementation for create channel (OkHttpStompMessageChannel) for subscribe to * queue by destination. - * @see StompMessageChannel + * @see OkHttpStompMessageChannel * * MessageMetaDataFactory is optional factory for create custom header for each message which will be sent * by this StompMessageChannel. @@ -22,15 +25,15 @@ private typealias MessageMetaDataFactory = (Channel, Message) -> StompDestinatio * DestinationOpenRequestHeaderFactory is optional factory for create open request header which will be sent * with subscribe message. */ -class StompDestination( +class OkHttpStompDestination( private val destination: String, private val openRequestFactory: DestinationOpenRequestHeaderFactory? = null, private val createMessageMetaDataCallable: MessageMetaDataFactory? = null ) : Protocol { override fun createChannelFactory() = SimpleChannelFactory { listener, parent -> - require(parent is StompMainChannel) - StompMessageChannel(destination, parent, parent, listener) + require(parent is OkHttpStompMainChannel) + OkHttpStompMessageChannel(destination, parent, parent, listener) } override fun createOpenRequestFactory(channel: Channel) = SimpleProtocolOpenRequestFactory { diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompMainChannel.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/client/OkHttpStompMainChannel.kt similarity index 83% rename from scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompMainChannel.kt rename to scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/client/OkHttpStompMainChannel.kt index a42b9ffb..0745ca64 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompMainChannel.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/client/OkHttpStompMainChannel.kt @@ -1,15 +1,21 @@ -package com.tinder.scarlet.stomp.core +/* + * © 2018 Match Group, LLC. + */ +package com.tinder.scarlet.stomp.okhttp.client import com.tinder.scarlet.Channel import com.tinder.scarlet.Protocol -import com.tinder.scarlet.stomp.core.models.StompCommand -import com.tinder.scarlet.stomp.core.models.StompHeader -import com.tinder.scarlet.stomp.core.models.StompMessage -import com.tinder.scarlet.stomp.okhttp.MessageHandler -import com.tinder.scarlet.stomp.okhttp.OkHttpStompClient -import com.tinder.scarlet.stomp.okhttp.WebSocketConnection -import com.tinder.scarlet.stomp.okhttp.WebSocketFactory -import com.tinder.scarlet.stomp.support.StompHeaderAccessor +import com.tinder.scarlet.stomp.okhttp.core.Connection +import com.tinder.scarlet.stomp.okhttp.core.IdGenerator +import com.tinder.scarlet.stomp.okhttp.core.MessageHandler +import com.tinder.scarlet.stomp.okhttp.core.StompListener +import com.tinder.scarlet.stomp.okhttp.core.StompSender +import com.tinder.scarlet.stomp.okhttp.core.StompSubscriber +import com.tinder.scarlet.stomp.okhttp.core.WebSocketFactory +import com.tinder.scarlet.stomp.okhttp.models.StompCommand +import com.tinder.scarlet.stomp.okhttp.models.StompHeader +import com.tinder.scarlet.stomp.okhttp.models.StompMessage +import com.tinder.scarlet.stomp.okhttp.support.StompHeaderAccessor import okhttp3.Response import okhttp3.WebSocket import okhttp3.WebSocketListener @@ -21,12 +27,13 @@ import kotlin.math.max * The main channel, which is responsible for connecting and disconnecting to the stomp server. * And also for sending messages and the logic of subscriptions. */ -class StompMainChannel( +class OkHttpStompMainChannel( private val configuration: Configuration, private val idGenerator: IdGenerator, private val webSocketFactory: WebSocketFactory, private val listener: Channel.Listener -) : Channel, StompSender, StompSubscriber { +) : Channel, StompSender, + StompSubscriber { companion object { @@ -116,8 +123,7 @@ class StompMainChannel( } override fun unsubscribe(destination: String) { - val subscriptionId = topicIds.remove(destination) - ?: throw IllegalStateException("Unknown destination=$destination") + val subscriptionId = topicIds.remove(destination) ?: return val stompHeaders = StompHeaderAccessor.of() .apply { this.subscriptionId = subscriptionId } @@ -161,7 +167,7 @@ class StompMainChannel( connection?.onReceiveInactivity(interval) { sendErrorMessage("No messages received in $interval ms.") connection?.close() - listener.onFailed(this@StompMainChannel, true, null) + listener.onFailed(this@OkHttpStompMainChannel, true, null) } } } @@ -191,8 +197,8 @@ class StompMainChannel( override fun onOpen(webSocket: WebSocket, response: Response) { val webSocketConnection = WebSocketConnection(webSocket) - this@StompMainChannel.connection = webSocketConnection - this@StompMainChannel.messageHandler = webSocketConnection + this@OkHttpStompMainChannel.connection = webSocketConnection + this@OkHttpStompMainChannel.messageHandler = webSocketConnection val host = configuration.host val login = openRequest.login @@ -210,17 +216,17 @@ class StompMainChannel( } override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { - listener.onClosing(this@StompMainChannel) + listener.onClosing(this@OkHttpStompMainChannel) } override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { - listener.onClosed(this@StompMainChannel) - this@StompMainChannel.connection = null + listener.onClosed(this@OkHttpStompMainChannel) + this@OkHttpStompMainChannel.connection = null } override fun onFailure(webSocket: WebSocket, throwable: Throwable, response: Response?) { - listener.onFailed(this@StompMainChannel, true, throwable) - this@StompMainChannel.connection = null + listener.onFailed(this@OkHttpStompMainChannel, true, throwable) + this@OkHttpStompMainChannel.connection = null } } @@ -228,7 +234,8 @@ class StompMainChannel( val stompHeaderAccessor = StompHeaderAccessor.of() .apply { this.host = host - this.acceptVersion = ACCEPT_VERSION + this.acceptVersion = + ACCEPT_VERSION this.login = login this.passcode = passcode } @@ -237,7 +244,7 @@ class StompMainChannel( val clientReceiveInterval = configuration.heartbeatReceiveInterval if (clientSendInterval > 0 && clientReceiveInterval > 0) { - stompHeaderAccessor.heartBeat(clientSendInterval, clientReceiveInterval) + stompHeaderAccessor.heartBeat = clientSendInterval to clientReceiveInterval } val stompMessage = StompMessage.Builder() @@ -270,7 +277,7 @@ class StompMainChannel( listener: Channel.Listener, parent: Channel? ): Channel? { - return StompMainChannel( + return OkHttpStompMainChannel( configuration = configuration, idGenerator = idGenerator, webSocketFactory = webSocketFactory, diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompMessageChannel.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/client/OkHttpStompMessageChannel.kt similarity index 71% rename from scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompMessageChannel.kt rename to scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/client/OkHttpStompMessageChannel.kt index cbec36b5..f0220d9a 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompMessageChannel.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/client/OkHttpStompMessageChannel.kt @@ -1,14 +1,19 @@ -package com.tinder.scarlet.stomp.core +/* + * © 2018 Match Group, LLC. + */ +package com.tinder.scarlet.stomp.okhttp.client import com.tinder.scarlet.Channel import com.tinder.scarlet.Message import com.tinder.scarlet.MessageQueue import com.tinder.scarlet.Protocol +import com.tinder.scarlet.stomp.okhttp.core.StompSender +import com.tinder.scarlet.stomp.okhttp.core.StompSubscriber /** * This channel is responsible for a specific subscription and its life cycle. */ -class StompMessageChannel( +class OkHttpStompMessageChannel( private val destination: String, private val stompSubscriber: StompSubscriber, private val stompSender: StompSender, @@ -18,14 +23,16 @@ class StompMessageChannel( private var messageQueueListener: MessageQueue.Listener? = null override fun open(openRequest: Protocol.OpenRequest) { - val destinationOpenRequest = openRequest as StompDestination.DestinationOpenRequest - val stompHeaders = destinationOpenRequest.headers + val destinationOpenRequest = openRequest as? OkHttpStompDestination.DestinationOpenRequest + val stompHeaders = destinationOpenRequest?.headers stompSubscriber.subscribe(destination, stompHeaders) { message -> messageQueueListener?.onMessageReceived( channel = this, messageQueue = this, message = Message.Text(message.payload.toString(Charsets.UTF_8)), - metadata = StompDestination.MessageMetaData(message.headers) + metadata = OkHttpStompDestination.MessageMetaData( + message.headers + ) ) } listener.onOpened(this) @@ -51,11 +58,11 @@ class StompMessageChannel( messageMetaData: Protocol.MessageMetaData ): Boolean = when (message) { is Message.Text -> { - val metaData = messageMetaData as? StompDestination.MessageMetaData + val metaData = messageMetaData as? OkHttpStompDestination.MessageMetaData stompSender.convertAndSend(message.value.toByteArray(), destination, metaData?.headers) } is Message.Bytes -> { - val metaData = messageMetaData as? StompDestination.MessageMetaData + val metaData = messageMetaData as? OkHttpStompDestination.MessageMetaData stompSender.convertAndSend(message.value, destination, metaData?.headers) } } diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/WebSocketConnection.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/client/WebSocketConnection.kt similarity index 82% rename from scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/WebSocketConnection.kt rename to scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/client/WebSocketConnection.kt index ba87e273..af554214 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/WebSocketConnection.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/client/WebSocketConnection.kt @@ -1,11 +1,14 @@ -package com.tinder.scarlet.stomp.okhttp +/* + * © 2018 Match Group, LLC. + */ +package com.tinder.scarlet.stomp.okhttp.client -import com.tinder.scarlet.stomp.core.Connection -import com.tinder.scarlet.stomp.core.models.StompMessage -import com.tinder.scarlet.stomp.support.StompMessageDecoder -import com.tinder.scarlet.stomp.support.StompMessageEncoder +import com.tinder.scarlet.stomp.okhttp.core.Connection +import com.tinder.scarlet.stomp.okhttp.core.MessageHandler +import com.tinder.scarlet.stomp.okhttp.models.StompMessage +import com.tinder.scarlet.stomp.okhttp.support.StompMessageDecoder +import com.tinder.scarlet.stomp.okhttp.support.StompMessageEncoder import okhttp3.WebSocket -import okio.ByteString import java.util.concurrent.Executors import java.util.concurrent.TimeUnit @@ -42,8 +45,7 @@ class WebSocketConnection( this.lastWriteTime = System.currentTimeMillis() } val encodedMessage = messageEncoder.encode(message) - val byteString = ByteString.of(encodedMessage, 0, encodedMessage.size) - return webSocket.send(byteString) + return webSocket.send(String(encodedMessage)) } /** @@ -89,7 +91,7 @@ class WebSocketConnection( /** * {@inheritDoc} */ - override fun handle(data: ByteArray): StompMessage { + override fun handle(data: ByteArray): StompMessage? { val lastReadTime = lastReadTime if (lastReadTime != -1L) { this.lastReadTime = System.currentTimeMillis() diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/Connection.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/core/Connection.kt similarity index 86% rename from scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/Connection.kt rename to scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/core/Connection.kt index c197c948..92951c13 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/Connection.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/core/Connection.kt @@ -1,6 +1,9 @@ -package com.tinder.scarlet.stomp.core +/* + * © 2018 Match Group, LLC. + */ +package com.tinder.scarlet.stomp.okhttp.core -import com.tinder.scarlet.stomp.core.models.StompMessage +import com.tinder.scarlet.stomp.okhttp.models.StompMessage interface Connection { diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/core/IdGenerator.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/core/IdGenerator.kt new file mode 100644 index 00000000..1d696194 --- /dev/null +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/core/IdGenerator.kt @@ -0,0 +1,12 @@ +/* + * © 2018 Match Group, LLC. + */ +package com.tinder.scarlet.stomp.okhttp.core + +interface IdGenerator { + + /** + * Generate a new identifier. + */ + fun generateId(): String +} \ No newline at end of file diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/core/MessageHandler.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/core/MessageHandler.kt new file mode 100644 index 00000000..ddf64c00 --- /dev/null +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/core/MessageHandler.kt @@ -0,0 +1,14 @@ +/* + * © 2018 Match Group, LLC. + */ +package com.tinder.scarlet.stomp.okhttp.core + +import com.tinder.scarlet.stomp.okhttp.models.StompMessage + +interface MessageHandler { + + /** + * Convert given raw data byte array to stomp message + */ + fun handle(data: ByteArray): StompMessage? +} \ No newline at end of file diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompSender.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/core/StompSender.kt similarity index 79% rename from scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompSender.kt rename to scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/core/StompSender.kt index fde977a8..1edd22a4 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompSender.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/core/StompSender.kt @@ -1,6 +1,9 @@ -package com.tinder.scarlet.stomp.core +/* + * © 2018 Match Group, LLC. + */ +package com.tinder.scarlet.stomp.okhttp.core -import com.tinder.scarlet.stomp.core.models.StompHeader +import com.tinder.scarlet.stomp.okhttp.models.StompHeader /** * Operations for sending messages to a destination. diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompSubscriber.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/core/StompSubscriber.kt similarity index 67% rename from scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompSubscriber.kt rename to scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/core/StompSubscriber.kt index 785b1615..c2f326a7 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/StompSubscriber.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/core/StompSubscriber.kt @@ -1,7 +1,10 @@ -package com.tinder.scarlet.stomp.core +/* + * © 2018 Match Group, LLC. + */ +package com.tinder.scarlet.stomp.okhttp.core -import com.tinder.scarlet.stomp.core.models.StompHeader -import com.tinder.scarlet.stomp.core.models.StompMessage +import com.tinder.scarlet.stomp.okhttp.models.StompHeader +import com.tinder.scarlet.stomp.okhttp.models.StompMessage typealias StompListener = (StompMessage) -> Unit diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/WebSocketFactory.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/core/WebSocketFactory.kt similarity index 85% rename from scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/WebSocketFactory.kt rename to scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/core/WebSocketFactory.kt index 65d1df2b..80b6cacc 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/WebSocketFactory.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/core/WebSocketFactory.kt @@ -1,7 +1,7 @@ /* * © 2018 Match Group, LLC. */ -package com.tinder.scarlet.stomp.okhttp +package com.tinder.scarlet.stomp.okhttp.core import okhttp3.Request import okhttp3.WebSocketListener diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/generator/UuidGenerator.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/generator/UuidGenerator.kt new file mode 100644 index 00000000..46ee9815 --- /dev/null +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/generator/UuidGenerator.kt @@ -0,0 +1,9 @@ +package com.tinder.scarlet.stomp.okhttp.generator + +import com.tinder.scarlet.stomp.okhttp.core.IdGenerator +import java.util.UUID + +class UuidGenerator : IdGenerator { + + override fun generateId() = UUID.randomUUID().toString() +} \ No newline at end of file diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/models/StompCommand.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/models/StompCommand.kt similarity index 85% rename from scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/models/StompCommand.kt rename to scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/models/StompCommand.kt index 2d6758a4..d8251347 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/models/StompCommand.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/models/StompCommand.kt @@ -1,4 +1,7 @@ -package com.tinder.scarlet.stomp.core.models +/* + * © 2018 Match Group, LLC. + */ +package com.tinder.scarlet.stomp.okhttp.models /** * Represents a STOMP command. diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/models/StompHeader.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/models/StompHeader.kt similarity index 98% rename from scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/models/StompHeader.kt rename to scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/models/StompHeader.kt index 41ae250b..cc82d9d5 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/models/StompHeader.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/models/StompHeader.kt @@ -2,7 +2,7 @@ * © 2018 Match Group, LLC. */ -package com.tinder.scarlet.stomp.core.models +package com.tinder.scarlet.stomp.okhttp.models /** * Represents STOMP frame headers. diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/models/StompMessage.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/models/StompMessage.kt similarity index 92% rename from scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/models/StompMessage.kt rename to scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/models/StompMessage.kt index 9044b53c..40dd3bef 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/core/models/StompMessage.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/models/StompMessage.kt @@ -1,6 +1,9 @@ -package com.tinder.scarlet.stomp.core.models +/* + * © 2018 Match Group, LLC. + */ +package com.tinder.scarlet.stomp.okhttp.models -import com.tinder.scarlet.stomp.support.StompHeaderAccessor +import com.tinder.scarlet.stomp.okhttp.support.StompHeaderAccessor /** * Stomp message representation with headers, payload and command. diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompHeaderAccessor.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/support/StompHeaderAccessor.kt similarity index 79% rename from scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompHeaderAccessor.kt rename to scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/support/StompHeaderAccessor.kt index c31d2925..1a2ae0d7 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompHeaderAccessor.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/support/StompHeaderAccessor.kt @@ -1,6 +1,9 @@ -package com.tinder.scarlet.stomp.support +/* + * © 2018 Match Group, LLC. + */ +package com.tinder.scarlet.stomp.okhttp.support -import com.tinder.scarlet.stomp.core.models.StompHeader +import com.tinder.scarlet.stomp.okhttp.models.StompHeader /** * A base for classes providing strongly typed getters and setters as well as @@ -29,6 +32,14 @@ class StompHeaderAccessor private constructor(headers: Map) { mutableHeaders[key] = value } + fun putAll(headers: Map) { + mutableHeaders.putAll(headers) + } + + fun message(message: String) { + mutableHeaders[STOMP_MESSAGE_HEADER] = message + } + var contentLength: Int? get() = try { mutableHeaders[StompHeader.CONTENT_LENGTH]?.toInt() @@ -39,13 +50,21 @@ class StompHeaderAccessor private constructor(headers: Map) { mutableHeaders[StompHeader.CONTENT_LENGTH] = value.toString() } - fun heartBeat(sendInterval: Long, receiveInterval: Long) { - mutableHeaders[StompHeader.HEARTBEAT] = "$sendInterval,$receiveInterval" - } + var heartBeat: Pair? + get() = mutableHeaders[StompHeader.HEARTBEAT] + ?.split(",") + ?.let { (sendIntervalStr, receiveIntervalStr) -> + val sendInterval = sendIntervalStr.toLongOrNull() ?: 0 + val receiveInterval = receiveIntervalStr.toLongOrNull() ?: 0 - fun putAll(headers: Map) { - mutableHeaders.putAll(headers) - } + sendInterval to receiveInterval + } + set(value) { + if (value != null) { + val (sendInterval, receiveInterval) = value + mutableHeaders[StompHeader.HEARTBEAT] = "$sendInterval,$receiveInterval" + } + } var subscriptionId: String? get() = mutableHeaders[StompHeader.ID] @@ -103,9 +122,5 @@ class StompHeaderAccessor private constructor(headers: Map) { } } - fun message(message: String) { - mutableHeaders[STOMP_MESSAGE_HEADER] = message - } - fun createHeader() = StompHeader(mutableHeaders) } \ No newline at end of file diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompMessageDecoder.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/support/StompMessageDecoder.kt similarity index 59% rename from scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompMessageDecoder.kt rename to scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/support/StompMessageDecoder.kt index d548b981..119827ca 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompMessageDecoder.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/support/StompMessageDecoder.kt @@ -1,7 +1,10 @@ -package com.tinder.scarlet.stomp.support +/* + * © 2018 Match Group, LLC. + */ +package com.tinder.scarlet.stomp.okhttp.support -import com.tinder.scarlet.stomp.core.models.StompCommand -import com.tinder.scarlet.stomp.core.models.StompMessage +import com.tinder.scarlet.stomp.okhttp.models.StompCommand +import com.tinder.scarlet.stomp.okhttp.models.StompMessage import java.io.ByteArrayOutputStream import java.nio.ByteBuffer @@ -10,40 +13,36 @@ import java.nio.ByteBuffer */ class StompMessageDecoder { - fun decode(data: ByteArray): StompMessage { - val byteBuffer = ByteBuffer.wrap(data) + /** + * Decodes the given byte array into a StompMessage. + * @param array the array to decode + */ + fun decode(array: ByteArray): StompMessage? { + val byteBuffer = ByteBuffer.wrap(array) return decode(byteBuffer) } - private fun decode(byteBuffer: ByteBuffer): StompMessage { + private fun decode(byteBuffer: ByteBuffer): StompMessage? { skipLeadingEol(byteBuffer) - val command = readCommand(byteBuffer) - - return if (command.isNotEmpty()) { + val stompCommand = readCommand(byteBuffer) ?: return null - val stompCommand = StompCommand.valueOf(command) + return if (stompCommand != StompCommand.HEARTBEAT) { val headerAccessor = StompHeaderAccessor.of() - val payload: ByteArray? = null - if (byteBuffer.isNotEmpty()) { + val payload = if (byteBuffer.isNotEmpty()) { readHeaders(byteBuffer, headerAccessor) - val payload = readPayload(byteBuffer, headerAccessor) - - if (payload != null) { - if (payload.isNotEmpty()) { - if (!stompCommand.isBodyAllowed) { - throw IllegalStateException(stompCommand.toString() + " shouldn't have a payload: length=" + payload.size + ", headers=" + headerAccessor) - } - } - } else { - byteBuffer.reset() - } + readPayloadOrNull(byteBuffer, headerAccessor) ?: return null + } else { + ByteArray(0) } + StompMessage.Builder() - .withPayload(payload ?: ByteArray(0)) + .withHeaders(headerAccessor.createHeader()) + .withPayload(payload) .create(stompCommand) } else { - StompMessage.Builder().create(StompCommand.UNKNOWN) + StompMessage.Builder() + .create(StompCommand.HEARTBEAT) } } @@ -53,31 +52,40 @@ class StompMessageDecoder { } } - private fun readPayload( + private fun readPayloadOrNull( byteBuffer: ByteBuffer, headerAccessor: StompHeaderAccessor ): ByteArray? { val contentLength = headerAccessor.contentLength - if (contentLength != null && contentLength >= 0) { - return if (byteBuffer.remaining() > contentLength) { - val payload = ByteArray(contentLength) - byteBuffer[payload] - check( - byteBuffer.get().toInt() == 0 - ) { "Frame must be terminated with a null octet" } - payload - } else { - null - } + return if (contentLength != null && contentLength >= 0) { + readPayloadWithContentLength(byteBuffer, contentLength) } else { - val payload = ByteArrayOutputStream(256) - while (byteBuffer.remaining() > 0) { - val b = byteBuffer.get() - if (b.toInt() == 0) { - return payload.toByteArray() - } else { - payload.write(b.toInt()) - } + readPayloadWithoutContentLength(byteBuffer) + } + } + + private fun readPayloadWithContentLength( + byteBuffer: ByteBuffer, + contentLength: Int + ) = byteBuffer + .takeIf { buffer -> buffer.remaining() > contentLength } + ?.let { buffer -> + val payload = ByteArray(contentLength) + buffer.get(payload) + + val lastSymbolIsNullOctet = byteBuffer.get().toInt() == 0 + check(lastSymbolIsNullOctet) { "Frame must be terminated with a null octet" } + payload + } + + private fun readPayloadWithoutContentLength(byteBuffer: ByteBuffer): ByteArray? { + val payload = ByteArrayOutputStream(256) + while (byteBuffer.isNotEmpty()) { + val byte = byteBuffer.get() + if (byte.toInt() != 0) { + payload.write(byte.toInt()) + } else { + return payload.toByteArray() } } return null @@ -87,6 +95,7 @@ class StompMessageDecoder { while (true) { val headerStream = ByteArrayOutputStream(256) var headerComplete = false + while (byteBuffer.hasRemaining()) { if (tryConsumeEndOfLine(byteBuffer)) { headerComplete = true @@ -94,15 +103,18 @@ class StompMessageDecoder { } headerStream.write(byteBuffer.get().toInt()) } + if (headerStream.size() > 0 && headerComplete) { - val header = String(headerStream.toByteArray(), Charsets.UTF_8) + val header = headerStream.toByteArray().toString(Charsets.UTF_8) val colonIndex = header.indexOf(':') - if (colonIndex <= 0) { - if (byteBuffer.isNotEmpty()) throw IllegalStateException("Illegal header: '$header'. A header must be of the form :[].") - } else { + + if (colonIndex > 0) { val headerName = unescape(header.substring(0, colonIndex)) val headerValue = unescape(header.substring(colonIndex + 1)) + headerAccessor[headerName] = headerValue + } else { + check(byteBuffer.isEmpty()) { "Illegal header: '$header'. A header must be of the form :[]." } } } else { break @@ -110,12 +122,21 @@ class StompMessageDecoder { } } - private fun readCommand(byteBuffer: ByteBuffer): String { + private fun readCommand(byteBuffer: ByteBuffer): StompCommand? { val command = ByteArrayOutputStream(256) while (byteBuffer.isNotEmpty() && !tryConsumeEndOfLine(byteBuffer)) { command.write(byteBuffer.get().toInt()) } - return String(command.toByteArray(), Charsets.UTF_8) + val commandString = command.toByteArray().toString(Charsets.UTF_8) + return try { + if (commandString.isNotEmpty()) { + StompCommand.valueOf(commandString) + } else { + StompCommand.HEARTBEAT + } + } catch (ex: Exception) { + null + } } /** @@ -168,4 +189,6 @@ class StompMessageDecoder { } private fun ByteBuffer.isNotEmpty(): Boolean = remaining() > 0 + + private fun ByteBuffer.isEmpty(): Boolean = remaining() == 0 } diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompMessageEncoder.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/support/StompMessageEncoder.kt similarity index 93% rename from scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompMessageEncoder.kt rename to scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/support/StompMessageEncoder.kt index 876abc1d..d4e19538 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/support/StompMessageEncoder.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/support/StompMessageEncoder.kt @@ -1,8 +1,11 @@ -package com.tinder.scarlet.stomp.support +/* + * © 2018 Match Group, LLC. + */ +package com.tinder.scarlet.stomp.okhttp.support -import com.tinder.scarlet.stomp.core.models.StompCommand -import com.tinder.scarlet.stomp.core.models.StompHeader -import com.tinder.scarlet.stomp.core.models.StompMessage +import com.tinder.scarlet.stomp.okhttp.models.StompCommand +import com.tinder.scarlet.stomp.okhttp.models.StompHeader +import com.tinder.scarlet.stomp.okhttp.models.StompMessage import java.io.ByteArrayOutputStream import java.io.DataOutputStream diff --git a/scarlet-protocol-stomp/src/test/java/com/tinder/scarlet/stomp/support/StompMessageDecoderTest.kt b/scarlet-protocol-stomp/src/test/java/com/tinder/scarlet/stomp/support/StompMessageDecoderTest.kt new file mode 100644 index 00000000..8e9c74d7 --- /dev/null +++ b/scarlet-protocol-stomp/src/test/java/com/tinder/scarlet/stomp/support/StompMessageDecoderTest.kt @@ -0,0 +1,197 @@ +/* + * © 2018 Match Group, LLC. + */ +package com.tinder.scarlet.stomp.support + +import com.tinder.scarlet.stomp.okhttp.models.StompCommand +import com.tinder.scarlet.stomp.okhttp.models.StompHeader +import com.tinder.scarlet.stomp.okhttp.models.StompMessage +import com.tinder.scarlet.stomp.okhttp.support.StompMessageDecoder +import junit.framework.Assert.assertEquals +import junit.framework.Assert.assertNull +import org.junit.Test + +class StompMessageDecoderTest { + + private val decoder = StompMessageDecoder() + + @Test + fun `decode message with CRLF EOls`() { + val message = decode("DISCONNECT\r\n\r\n\u0000") + assertEquals( + StompCommand.DISCONNECT, + message.command + ) + assertEquals(0, message.headers.size) + assertEquals(0, message.payload.size) + } + + @Test + fun `decode message with no headers and no body`() { + val message = decode("DISCONNECT\n\n\u0000") + assertEquals( + StompCommand.DISCONNECT, + message.command + ) + assertEquals(0, message.headers.size) + assertEquals(0, message.payload.size) + } + + @Test + fun `decode message with no body`() { + val accept = "accept-version:1.1\n" + val host = "host:github.org\n" + val message = decode("CONNECT\n$accept$host\n\u0000") + + assertEquals( + StompCommand.CONNECT, + message.command + ) + assertEquals(2, message.headers.size) + assertEquals("1.1", message.headers[StompHeader.ACCEPT_VERSION]) + assertEquals("github.org", message.headers[StompHeader.HOST]) + assertEquals(0, message.payload.size) + } + + @Test + fun `decode message`() { + val message = decode("MESSAGE\ndestination:test\n\nThe body of the message\u0000") + + assertEquals(StompCommand.MESSAGE, message.command) + assertEquals(message.headers.toString(), 1, message.headers.size) + assertEquals("test", message.headers.destination) + + val bodyText = message.payload.toString(Charsets.UTF_8) + assertEquals("The body of the message", bodyText) + } + + @Test + fun `decode message with content length`() { + val message = + decode("MESSAGE\ndestination:test\ncontent-length:23\n\nThe body of the message\u0000") + + assertEquals(StompCommand.MESSAGE, message.command) + assertEquals(2, message.headers.size) + assertEquals(23, message.headers[StompHeader.CONTENT_LENGTH]?.toInt()) + + val bodyText = message.payload.toString(Charsets.UTF_8) + assertEquals("The body of the message", bodyText) + } + + @Test + fun `decode frame with invalid content length`() { + val message = + decode("MESSAGE\ndestination:test\ncontent-length:-1\n\nThe body of the message\u0000") + + assertEquals(StompCommand.MESSAGE, message.command) + assertEquals(2, message.headers.size) + assertEquals(-1, message.headers[StompHeader.CONTENT_LENGTH]?.toInt()) + + val bodyText = message.payload.toString(Charsets.UTF_8) + assertEquals("The body of the message", bodyText) + } + + @Test + fun `decode frame with content length zero`() { + val message = decode("MESSAGE\ndestination:test\ncontent-length:0\n\n\u0000") + + assertEquals(StompCommand.MESSAGE, message.command) + assertEquals(2, message.headers.size) + assertEquals(0, message.headers[StompHeader.CONTENT_LENGTH]?.toInt()) + + val bodyText = message.payload.toString(Charsets.UTF_8) + assertEquals("", bodyText) + } + + @Test + fun `decode frame with null octects in the body`() { + val message = + decode("MESSAGE\ndestination:test\ncontent-length:23\n\nThe b\u0000dy \u0000f the message\u0000") + + assertEquals(StompCommand.MESSAGE, message.command) + assertEquals(2, message.headers.size) + assertEquals(23, message.headers[StompHeader.CONTENT_LENGTH]?.toInt()) + + val bodyText = message.payload.toString(Charsets.UTF_8) + assertEquals("The b\u0000dy \u0000f the message", bodyText) + } + + @Test + fun `decode frame with escaped headers`() { + val message = + decode("CONNECTED\na\\c\\r\\n\\\\b:alpha\\cbravo\\r\\n\\\\\n\n\u0000") + assertEquals( + StompCommand.CONNECTED, + message.command + ) + assertEquals(1, message.headers.size) + assertEquals("alpha:bravo\r\n\\", message.headers["a:\r\n\\b"]) + } + + @Test(expected = IllegalStateException::class) + fun `decode frame body not allowed`() { + decode("CONNECTED\naccept-version:1.2\n\nThe body of the message\u0000") + } + + @Test + fun `decode frame with header with empty value`() { + val accept = "accept-version:1.1\n" + val valuelessKey = "key:\n" + val message = decode("CONNECTED\n$accept$valuelessKey\n\u0000") + + assertEquals( + StompCommand.CONNECTED, + message.command + ) + assertEquals(2, message.headers.size) + assertEquals("1.1", message.headers["accept-version"]) + assertEquals("", message.headers["key"]) + assertEquals(0, message.payload.size) + } + + @Test + fun decodeFrameWithIncompleteCommand() { + assertIncompleteDecode("MESSAG") + } + + @Test + fun `decode frame with incomplete header`() { + assertIncompleteDecode("SEND\ndestination") + assertIncompleteDecode("SEND\ndestination:") + assertIncompleteDecode("SEND\ndestination:test") + } + + @Test + fun `decode frame without null octet terminator`() { + assertIncompleteDecode("SEND\ndestination:test\n") + assertIncompleteDecode("SEND\ndestination:test\n\n") + assertIncompleteDecode("SEND\ndestination:test\n\nThe body") + } + + @Test + fun `decode frame with insufficient content`() { + assertIncompleteDecode("SEND\ncontent-length:23\n\nThe body of the mess") + } + + @Test + fun decodeHeartbeat() { + val frame = "\n" + val array = frame.toByteArray() + val message = decoder.decode(array) + + assertEquals( + StompCommand.HEARTBEAT, + message?.command + ) + } + + private fun assertIncompleteDecode(frame: String) { + val message = decoder.decode(frame.toByteArray(Charsets.UTF_8)) + assertNull(message) + } + + private fun decode(stompFrame: String): StompMessage { + val array = stompFrame.toByteArray() + return array.let(decoder::decode)!! + } +} \ No newline at end of file diff --git a/scarlet-protocol-stomp/src/test/java/com/tinder/scarlet/stomp/support/StompMessageEncoderTest.kt b/scarlet-protocol-stomp/src/test/java/com/tinder/scarlet/stomp/support/StompMessageEncoderTest.kt index 8aee2888..e3722c90 100644 --- a/scarlet-protocol-stomp/src/test/java/com/tinder/scarlet/stomp/support/StompMessageEncoderTest.kt +++ b/scarlet-protocol-stomp/src/test/java/com/tinder/scarlet/stomp/support/StompMessageEncoderTest.kt @@ -1,9 +1,14 @@ +/* + * © 2018 Match Group, LLC. + */ package com.tinder.scarlet.stomp.support -import com.tinder.scarlet.stomp.core.models.StompCommand -import com.tinder.scarlet.stomp.core.models.StompMessage -import junit.framework.Assert.assertEquals -import junit.framework.Assert.assertTrue +import com.tinder.scarlet.stomp.okhttp.models.StompCommand +import com.tinder.scarlet.stomp.okhttp.models.StompMessage +import com.tinder.scarlet.stomp.okhttp.support.StompHeaderAccessor +import com.tinder.scarlet.stomp.okhttp.support.StompMessageEncoder +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue import org.junit.Test class StompMessageEncoderTest { From 7cef97ea4d11216115bc1cf9f0af5ce58b0cbd81 Mon Sep 17 00:00:00 2001 From: NikitaZaltsman Date: Sat, 14 Mar 2020 17:36:51 +0700 Subject: [PATCH 29/32] add integration test for ok http stomp --- scarlet-protocol-stomp/build.gradle | 3 + .../stomp/OkHttpStompIntegrationTest.kt | 93 +++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 scarlet-protocol-stomp/src/test/java/com/tinder/scarlet/stomp/OkHttpStompIntegrationTest.kt diff --git a/scarlet-protocol-stomp/build.gradle b/scarlet-protocol-stomp/build.gradle index 239d8406..fa130972 100644 --- a/scarlet-protocol-stomp/build.gradle +++ b/scarlet-protocol-stomp/build.gradle @@ -25,5 +25,8 @@ dependencies { testImplementation rootProject.ext.activeMqJUnit testImplementation rootProject.ext.activeMqBroker testImplementation rootProject.ext.activeMqStomp + testImplementation rootProject.ext.activeHttp testImplementation rootProject.ext.slf4jSimple + + testImplementation "javax.servlet:javax.servlet-api:4.0.1" } diff --git a/scarlet-protocol-stomp/src/test/java/com/tinder/scarlet/stomp/OkHttpStompIntegrationTest.kt b/scarlet-protocol-stomp/src/test/java/com/tinder/scarlet/stomp/OkHttpStompIntegrationTest.kt new file mode 100644 index 00000000..801cb6f1 --- /dev/null +++ b/scarlet-protocol-stomp/src/test/java/com/tinder/scarlet/stomp/OkHttpStompIntegrationTest.kt @@ -0,0 +1,93 @@ +package com.tinder.scarlet.stomp + +import com.tinder.scarlet.ProtocolEvent +import com.tinder.scarlet.Stream +import com.tinder.scarlet.testutils.rule.OkHttpStompWebSocketConnection +import com.tinder.scarlet.testutils.test +import com.tinder.scarlet.ws.Receive +import com.tinder.scarlet.ws.Send +import org.apache.activemq.broker.jmx.ManagementContext +import org.apache.activemq.command.ActiveMQTopic +import org.apache.activemq.junit.EmbeddedActiveMQBroker +import org.junit.Rule +import org.junit.Test +import java.util.logging.Logger + + +class OkHttpStompIntegrationTest { + + @get:Rule + val broker = object : EmbeddedActiveMQBroker() { + override fun configure() { + brokerService.addConnector(BROKER_URL) + + val topic = ActiveMQTopic(DESTINATION) + brokerService.destinations = arrayOf(topic) + + val managementContext = ManagementContext() + managementContext.isCreateConnector = true + brokerService.managementContext = managementContext + } + } + + @get:Rule + val firstConnection = OkHttpStompWebSocketConnection.create( + { observeProtocolEvent() }, + OkHttpStompWebSocketConnection.Configuration( + login = LOGIN, + port = PORT, + password = PASSWORD, + host = HOST, + destination = DESTINATION + ) + ) + + @get:Rule + val secondConnection = OkHttpStompWebSocketConnection.create( + { observeProtocolEvent() }, + OkHttpStompWebSocketConnection.Configuration( + login = LOGIN, + port = PORT, + password = PASSWORD, + host = HOST, + destination = DESTINATION + ) + ) + + @Test + fun `correct receive and send messages`() { + val queueTextObserver = secondConnection.client.observeText().test() + + firstConnection.open() + firstConnection.client.sendText("message1") + firstConnection.client.sendText("message2") + firstConnection.clientClosure() + + secondConnection.open() + + LOGGER.info("${queueTextObserver.values}") + queueTextObserver.awaitCount(2) + } + + companion object { + private val LOGGER = Logger.getLogger(OkHttpStompIntegrationTest::class.java.name) + + private const val HOST = "localhost" + private const val PORT = 34343 + private const val LOGIN = "system" + private const val PASSWORD = "manager" + private const val BROKER_URL = "ws://$HOST:$PORT" + private const val DESTINATION = "/queue/test" + + interface StompQueueTestService { + @Receive + fun observeProtocolEvent(): Stream + + @Receive + fun observeText(): Stream + + @Send + fun sendText(message: String) + } + } +} From 52b54c348b2cf14295bb75e3a6b5ee03f5d24c93 Mon Sep 17 00:00:00 2001 From: NikitaZaltsman Date: Sat, 14 Mar 2020 17:38:50 +0700 Subject: [PATCH 30/32] Add OkHttpStompWebSocketConnection rename test method add unsubscribeAll method add to string implementation for stomp header and messages add awaitCountAtLeast method move okhttp stomp integration test to another module remove extra line in test method Incapsulate the logic of calculating "whether to send a heartbeat settings to server" Add field shouldRetryAfterError Add setting shouldRetryAfterError to configuration Refactoring OkHttpStompMainChannel Add method disconnect with disposing logic Change InnerWebSocketListener class modifier to private Change string regex to use char Add overloading method "of" in StompHeaderAccessor --- .../jackson/JacksonMessageAdapterTest.kt | 4 +- .../mqtt/PahoMqttClientIntegrationTest.kt | 2 +- .../okhttp/client/OkHttpStompMainChannel.kt | 57 +++--- .../client/OkHttpStompMessageChannel.kt | 4 +- .../stomp/okhttp/models/StompHeader.kt | 6 +- .../stomp/okhttp/models/StompMessage.kt | 10 ++ .../okhttp/support/StompHeaderAccessor.kt | 5 +- .../scarlet/stomp/StompIntegrationTest.kt | 4 +- .../OkHttpStompIntegrationTest.kt | 26 +-- .../stomp/support/StompMessageEncoderTest.kt | 6 +- .../coroutines/FlowStreamAdapterTest.kt | 1 - .../scarlet/testutils/TestStreamObserver.kt | 10 +- .../rule/OkHttpStompWebSocketConnection.kt | 167 ++++++++++++++++++ 13 files changed, 247 insertions(+), 55 deletions(-) rename scarlet-protocol-stomp/src/test/java/com/tinder/scarlet/stomp/{ => okhttp}/OkHttpStompIntegrationTest.kt (80%) create mode 100644 scarlet-test-utils/src/main/java/com/tinder/scarlet/testutils/rule/OkHttpStompWebSocketConnection.kt diff --git a/scarlet-message-adapter-jackson/src/test/java/com/tinder/scarlet/messageadapter/jackson/JacksonMessageAdapterTest.kt b/scarlet-message-adapter-jackson/src/test/java/com/tinder/scarlet/messageadapter/jackson/JacksonMessageAdapterTest.kt index 52d7beda..a2357400 100644 --- a/scarlet-message-adapter-jackson/src/test/java/com/tinder/scarlet/messageadapter/jackson/JacksonMessageAdapterTest.kt +++ b/scarlet-message-adapter-jackson/src/test/java/com/tinder/scarlet/messageadapter/jackson/JacksonMessageAdapterTest.kt @@ -58,7 +58,7 @@ internal class JacksonMessageAdapterTest { any(), any().containingText(expectedString) ) - serverAnImplementationObserver.awaitCount(1) + serverAnImplementationObserver.awaitCountAndCheck(1) assertThat(serverAnImplementationObserver.values).containsExactly(data) } @@ -79,7 +79,7 @@ internal class JacksonMessageAdapterTest { any(), any().containingText(expectedString) ) - serverAnImplementationObserver.awaitCount(1) + serverAnImplementationObserver.awaitCountAndCheck(1) assertThat(serverAnImplementationObserver.values).containsExactly(data) } diff --git a/scarlet-protocol-mqtt/src/test/java/com/tinder/scarlet/mqtt/PahoMqttClientIntegrationTest.kt b/scarlet-protocol-mqtt/src/test/java/com/tinder/scarlet/mqtt/PahoMqttClientIntegrationTest.kt index 213b02de..1f2b51a3 100644 --- a/scarlet-protocol-mqtt/src/test/java/com/tinder/scarlet/mqtt/PahoMqttClientIntegrationTest.kt +++ b/scarlet-protocol-mqtt/src/test/java/com/tinder/scarlet/mqtt/PahoMqttClientIntegrationTest.kt @@ -55,7 +55,7 @@ class PahoMqttClientIntegrationTest { connection1.clientClosure() LOGGER.info("${queueTextObserver.values}") - queueTextObserver.awaitCount(2) + queueTextObserver.awaitCountAndCheck(2) } companion object { diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/client/OkHttpStompMainChannel.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/client/OkHttpStompMainChannel.kt index 0745ca64..1b6cf891 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/client/OkHttpStompMainChannel.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/client/OkHttpStompMainChannel.kt @@ -32,8 +32,7 @@ class OkHttpStompMainChannel( private val idGenerator: IdGenerator, private val webSocketFactory: WebSocketFactory, private val listener: Channel.Listener -) : Channel, StompSender, - StompSubscriber { +) : Channel, StompSender, StompSubscriber { companion object { @@ -59,10 +58,7 @@ class OkHttpStompMainChannel( } override fun forceClose() { - topicIds.clear() - subscriptions.clear() - - sendDisconnectMessage() + disconnect() connection?.forceClose() connection = null @@ -70,10 +66,7 @@ class OkHttpStompMainChannel( } override fun close(closeRequest: Protocol.CloseRequest) { - topicIds.clear() - subscriptions.clear() - - sendDisconnectMessage() + disconnect() connection?.close() connection = null @@ -147,7 +140,11 @@ class OkHttpStompMainChannel( val listener = subscriptions[destination] listener?.invoke(stompMessage) } - StompCommand.ERROR -> listener.onFailed(this, true, null) + StompCommand.ERROR -> listener.onFailed( + this, + configuration.shouldRetryAfterError, + null + ) else -> Unit // not a server message } @@ -191,9 +188,10 @@ class OkHttpStompMainChannel( connection?.sendMessage(stompMessage) } - inner class InnerWebSocketListener( + private inner class InnerWebSocketListener( private val openRequest: OkHttpStompClient.ClientOpenRequest ) : WebSocketListener() { + override fun onOpen(webSocket: WebSocket, response: Response) { val webSocketConnection = WebSocketConnection(webSocket) @@ -225,7 +223,11 @@ class OkHttpStompMainChannel( } override fun onFailure(webSocket: WebSocket, throwable: Throwable, response: Response?) { - listener.onFailed(this@OkHttpStompMainChannel, true, throwable) + listener.onFailed( + this@OkHttpStompMainChannel, + configuration.shouldRetryAfterError, + throwable + ) this@OkHttpStompMainChannel.connection = null } } @@ -234,17 +236,13 @@ class OkHttpStompMainChannel( val stompHeaderAccessor = StompHeaderAccessor.of() .apply { this.host = host - this.acceptVersion = - ACCEPT_VERSION + this.acceptVersion = ACCEPT_VERSION this.login = login this.passcode = passcode } - val clientSendInterval = configuration.heartbeatSendInterval - val clientReceiveInterval = configuration.heartbeatReceiveInterval - - if (clientSendInterval > 0 && clientReceiveInterval > 0) { - stompHeaderAccessor.heartBeat = clientSendInterval to clientReceiveInterval + if (configuration.shouldSendHeartBeat) { + stompHeaderAccessor.heartBeat = configuration.headerBeatPair } val stompMessage = StompMessage.Builder() @@ -254,6 +252,15 @@ class OkHttpStompMainChannel( connection?.sendMessage(stompMessage) } + private fun disconnect() { + subscriptions.keys.forEach(::unsubscribe) + + topicIds.clear() + subscriptions.clear() + + sendDisconnectMessage() + } + private fun sendDisconnectMessage() { val stompMessage = StompMessage.Builder() .create(StompCommand.DISCONNECT) @@ -263,9 +270,17 @@ class OkHttpStompMainChannel( data class Configuration( val host: String, + val shouldRetryAfterError: Boolean = true, val heartbeatSendInterval: Long = 0, val heartbeatReceiveInterval: Long = 0 - ) + ) { + + val shouldSendHeartBeat: Boolean + get() = heartbeatSendInterval > 0 && heartbeatReceiveInterval > 0 + + val headerBeatPair: Pair + get() = heartbeatSendInterval to heartbeatReceiveInterval + } class Factory( private val idGenerator: IdGenerator, diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/client/OkHttpStompMessageChannel.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/client/OkHttpStompMessageChannel.kt index f0220d9a..5062abc2 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/client/OkHttpStompMessageChannel.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/client/OkHttpStompMessageChannel.kt @@ -30,9 +30,7 @@ class OkHttpStompMessageChannel( channel = this, messageQueue = this, message = Message.Text(message.payload.toString(Charsets.UTF_8)), - metadata = OkHttpStompDestination.MessageMetaData( - message.headers - ) + metadata = OkHttpStompDestination.MessageMetaData(message.headers) ) } listener.onOpened(this) diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/models/StompHeader.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/models/StompHeader.kt index cc82d9d5..c8527d5d 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/models/StompHeader.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/models/StompHeader.kt @@ -102,7 +102,11 @@ class StompHeader( val heartBeat: Pair get() { val heartBeat = headers[HEARTBEAT] ?: return 0L to 0L - val (sendInterval, receiveInterval) = heartBeat.split(",") + val (sendInterval, receiveInterval) = heartBeat.split(',') return sendInterval.toLong() to receiveInterval.toLong() } + + override fun toString(): String { + return "StompHeader(headers=$headers)" + } } \ No newline at end of file diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/models/StompMessage.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/models/StompMessage.kt index 40dd3bef..daa52a6a 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/models/StompMessage.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/models/StompMessage.kt @@ -66,4 +66,14 @@ class StompMessage private constructor( result = 31 * result + headers.hashCode() return result } + + override fun toString(): String { + return "StompMessage(" + + "command=$command, " + + "payload=${payload.toString(Charsets.UTF_8)}, " + + "headers=$headers" + + ")" + } + + } \ No newline at end of file diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/support/StompHeaderAccessor.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/support/StompHeaderAccessor.kt index 1a2ae0d7..02e3ddae 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/support/StompHeaderAccessor.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/support/StompHeaderAccessor.kt @@ -20,6 +20,9 @@ class StompHeaderAccessor private constructor(headers: Map) { */ private const val STOMP_MESSAGE_HEADER = "message" + fun of(vararg headers: Pair): StompHeaderAccessor = + StompHeaderAccessor(mapOf(*headers)) + fun of(headers: Map = emptyMap()): StompHeaderAccessor = StompHeaderAccessor(headers) } @@ -52,7 +55,7 @@ class StompHeaderAccessor private constructor(headers: Map) { var heartBeat: Pair? get() = mutableHeaders[StompHeader.HEARTBEAT] - ?.split(",") + ?.split(',') ?.let { (sendIntervalStr, receiveIntervalStr) -> val sendInterval = sendIntervalStr.toLongOrNull() ?: 0 val receiveInterval = receiveIntervalStr.toLongOrNull() ?: 0 diff --git a/scarlet-protocol-stomp/src/test/java/com/tinder/scarlet/stomp/StompIntegrationTest.kt b/scarlet-protocol-stomp/src/test/java/com/tinder/scarlet/stomp/StompIntegrationTest.kt index 1d3ef4eb..d03c8418 100644 --- a/scarlet-protocol-stomp/src/test/java/com/tinder/scarlet/stomp/StompIntegrationTest.kt +++ b/scarlet-protocol-stomp/src/test/java/com/tinder/scarlet/stomp/StompIntegrationTest.kt @@ -58,7 +58,7 @@ class StompIntegrationTest { connection2.open() LOGGER.info("${queueTextObserver.values}") - queueTextObserver.awaitCount(2) + queueTextObserver.awaitCountAtLeast(1) // because broker has a bug and it loses messages sometimes } @Test @@ -82,7 +82,7 @@ class StompIntegrationTest { val queueTextObserver = connection2.client.observeText().test() - queueTextObserver.awaitCount(2) + queueTextObserver.awaitCountAndCheck(2) LOGGER.info("${queueTextObserver.values}") } diff --git a/scarlet-protocol-stomp/src/test/java/com/tinder/scarlet/stomp/OkHttpStompIntegrationTest.kt b/scarlet-protocol-stomp/src/test/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompIntegrationTest.kt similarity index 80% rename from scarlet-protocol-stomp/src/test/java/com/tinder/scarlet/stomp/OkHttpStompIntegrationTest.kt rename to scarlet-protocol-stomp/src/test/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompIntegrationTest.kt index 801cb6f1..7b07c309 100644 --- a/scarlet-protocol-stomp/src/test/java/com/tinder/scarlet/stomp/OkHttpStompIntegrationTest.kt +++ b/scarlet-protocol-stomp/src/test/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompIntegrationTest.kt @@ -1,4 +1,4 @@ -package com.tinder.scarlet.stomp +package com.tinder.scarlet.stomp.okhttp import com.tinder.scarlet.ProtocolEvent import com.tinder.scarlet.Stream @@ -6,32 +6,22 @@ import com.tinder.scarlet.testutils.rule.OkHttpStompWebSocketConnection import com.tinder.scarlet.testutils.test import com.tinder.scarlet.ws.Receive import com.tinder.scarlet.ws.Send -import org.apache.activemq.broker.jmx.ManagementContext -import org.apache.activemq.command.ActiveMQTopic import org.apache.activemq.junit.EmbeddedActiveMQBroker import org.junit.Rule import org.junit.Test import java.util.logging.Logger - class OkHttpStompIntegrationTest { @get:Rule val broker = object : EmbeddedActiveMQBroker() { override fun configure() { brokerService.addConnector(BROKER_URL) - - val topic = ActiveMQTopic(DESTINATION) - brokerService.destinations = arrayOf(topic) - - val managementContext = ManagementContext() - managementContext.isCreateConnector = true - brokerService.managementContext = managementContext } } @get:Rule - val firstConnection = OkHttpStompWebSocketConnection.create( + val firstConnection = OkHttpStompWebSocketConnection.create( { observeProtocolEvent() }, OkHttpStompWebSocketConnection.Configuration( login = LOGIN, @@ -43,7 +33,7 @@ class OkHttpStompIntegrationTest { ) @get:Rule - val secondConnection = OkHttpStompWebSocketConnection.create( + val secondConnection = OkHttpStompWebSocketConnection.create( { observeProtocolEvent() }, OkHttpStompWebSocketConnection.Configuration( login = LOGIN, @@ -59,14 +49,16 @@ class OkHttpStompIntegrationTest { val queueTextObserver = secondConnection.client.observeText().test() firstConnection.open() + secondConnection.open() + firstConnection.client.sendText("message1") firstConnection.client.sendText("message2") firstConnection.clientClosure() - secondConnection.open() - LOGGER.info("${queueTextObserver.values}") - queueTextObserver.awaitCount(2) + queueTextObserver.awaitCountAtLeast(1)// because broker has a bug and it loses messages sometimes + + secondConnection.clientClosure() } companion object { @@ -79,7 +71,7 @@ class OkHttpStompIntegrationTest { private const val BROKER_URL = "ws://$HOST:$PORT" private const val DESTINATION = "/queue/test" - interface StompQueueTestService { + interface StompOkHttpQueueTestService { @Receive fun observeProtocolEvent(): Stream diff --git a/scarlet-protocol-stomp/src/test/java/com/tinder/scarlet/stomp/support/StompMessageEncoderTest.kt b/scarlet-protocol-stomp/src/test/java/com/tinder/scarlet/stomp/support/StompMessageEncoderTest.kt index e3722c90..6e79a2e9 100644 --- a/scarlet-protocol-stomp/src/test/java/com/tinder/scarlet/stomp/support/StompMessageEncoderTest.kt +++ b/scarlet-protocol-stomp/src/test/java/com/tinder/scarlet/stomp/support/StompMessageEncoderTest.kt @@ -45,8 +45,7 @@ class StompMessageEncoderTest { @Test fun `encode frame with headers that should be escaped`() { - val headers = StompHeaderAccessor.of() - .apply { putAll(mapOf("a:\r\n\\b" to "alpha:bravo\r\n\\")) } + val headers = StompHeaderAccessor.of("a:\r\n\\b" to "alpha:bravo\r\n\\") .createHeader() val frame = StompMessage.Builder() @@ -62,8 +61,7 @@ class StompMessageEncoderTest { @Test fun `encode frame with headers body`() { - val headers = StompHeaderAccessor.of().apply { - putAll(mapOf("a" to "alpha")) + val headers = StompHeaderAccessor.of(("a" to "alpha")).apply { destination = "destination" }.createHeader() diff --git a/scarlet-stream-adapter-coroutines/src/test/java/com/tinder/scarlet/streamadapter/coroutines/FlowStreamAdapterTest.kt b/scarlet-stream-adapter-coroutines/src/test/java/com/tinder/scarlet/streamadapter/coroutines/FlowStreamAdapterTest.kt index 7e26b4c6..6f2c144d 100644 --- a/scarlet-stream-adapter-coroutines/src/test/java/com/tinder/scarlet/streamadapter/coroutines/FlowStreamAdapterTest.kt +++ b/scarlet-stream-adapter-coroutines/src/test/java/com/tinder/scarlet/streamadapter/coroutines/FlowStreamAdapterTest.kt @@ -47,7 +47,6 @@ class FlowStreamAdapterTest { testTextFlow.collect { receivedTextItems.add(it) } - } val job2 = launch { testBytesFlow.collect { diff --git a/scarlet-test-utils/src/main/java/com/tinder/scarlet/testutils/TestStreamObserver.kt b/scarlet-test-utils/src/main/java/com/tinder/scarlet/testutils/TestStreamObserver.kt index 71e14610..4133e23c 100644 --- a/scarlet-test-utils/src/main/java/com/tinder/scarlet/testutils/TestStreamObserver.kt +++ b/scarlet-test-utils/src/main/java/com/tinder/scarlet/testutils/TestStreamObserver.kt @@ -25,14 +25,20 @@ class TestStreamObserver(stream: Stream) { val completions: Long get() = testSubscriber.completions() - fun awaitCount(exactly: Int) { + fun awaitCountAndCheck(exactly: Int) { testSubscriber.assertNoErrors() testSubscriber.awaitCount(exactly) assertThat(values).describedAs("values: $values").hasSize(exactly) } + fun awaitCountAtLeast(atLeast: Int) { + testSubscriber.assertNoErrors() + testSubscriber.awaitCount(atLeast) + assertThat(values).isNotEmpty + } + fun awaitValues(vararg valueAsserts: ValueAssert) { - awaitCount(valueAsserts.size) + awaitCountAndCheck(valueAsserts.size) testSubscriber.assertNoErrors() valueAsserts.zip(values).forEachIndexed { index, (valueAssert, value) -> try { diff --git a/scarlet-test-utils/src/main/java/com/tinder/scarlet/testutils/rule/OkHttpStompWebSocketConnection.kt b/scarlet-test-utils/src/main/java/com/tinder/scarlet/testutils/rule/OkHttpStompWebSocketConnection.kt new file mode 100644 index 00000000..84bc6cb9 --- /dev/null +++ b/scarlet-test-utils/src/main/java/com/tinder/scarlet/testutils/rule/OkHttpStompWebSocketConnection.kt @@ -0,0 +1,167 @@ +/* + * © 2018 Match Group, LLC. + */ + +package com.tinder.scarlet.testutils.rule + +import com.tinder.scarlet.LifecycleState +import com.tinder.scarlet.MessageAdapter +import com.tinder.scarlet.ProtocolEvent +import com.tinder.scarlet.Scarlet +import com.tinder.scarlet.Stream +import com.tinder.scarlet.StreamAdapter +import com.tinder.scarlet.lifecycle.LifecycleRegistry +import com.tinder.scarlet.stomp.okhttp.client.OkHttpStompClient +import com.tinder.scarlet.stomp.okhttp.client.OkHttpStompDestination +import com.tinder.scarlet.stomp.okhttp.client.OkHttpStompMainChannel +import com.tinder.scarlet.testutils.TestStreamObserver +import com.tinder.scarlet.testutils.any +import com.tinder.scarlet.testutils.test +import okhttp3.OkHttpClient +import okhttp3.Request +import org.junit.rules.ExternalResource +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement +import java.util.concurrent.TimeUnit +import java.util.logging.Level +import java.util.logging.Logger + +class OkHttpStompWebSocketConnection( + private val clazz: Class, + private val observeProtocolEvent: SERVICE.() -> Stream, + private val clientConfiguration: Configuration +) : TestRule { + + val client: SERVICE + get() = clientAndServer.client + + val clientProtocolEventObserver: TestStreamObserver + get() = clientAndServer.clientProtocolEventObserver + + private val clientLifecycleRegistry = LifecycleRegistry() + + private val clientAndServer = ClientAndServer() + + override fun apply(base: Statement, description: Description): Statement { + return clientAndServer + .apply(base, description) + } + + fun open() { + clientLifecycleRegistry.onNext(LifecycleState.Started) + blockUntilConnectionIsEstablish() + } + + fun clientClosure() { + clientLifecycleRegistry.onNext(LifecycleState.Stopped) + } + + fun clientTerminate() { + clientLifecycleRegistry.onNext(LifecycleState.Completed) + } + + private fun blockUntilConnectionIsEstablish() { + clientProtocolEventObserver.awaitValues( + any() + ) + } + + private inner class ClientAndServer : ExternalResource() { + lateinit var client: SERVICE + lateinit var clientProtocolEventObserver: TestStreamObserver + + override fun before() { + createClientAndServer() + } + + override fun after() { + clientLifecycleRegistry.onNext(LifecycleState.Completed) + } + + private fun createClientAndServer() { + client = createClient() + clientProtocolEventObserver = client.observeProtocolEvent().test() + client.observeProtocolEvent().start(object : Stream.Observer { + override fun onNext(data: ProtocolEvent) { + LOGGER.info("$this: client stomp event: $data") + } + + override fun onError(throwable: Throwable) { + LOGGER.log( + Level.WARNING, + "$this: client stomp error", + throwable + ) + } + + override fun onComplete() { + LOGGER.info("client stomp completed") + } + }) + } + + private fun createClient(): SERVICE { + val protocol = OkHttpStompClient( + configuration = OkHttpStompMainChannel.Configuration( + host = "ws://${clientConfiguration.host}:${clientConfiguration.port}" + ), + okHttpClient = createOkHttpClient(), + requestFactory = { + OkHttpStompClient.ClientOpenRequest( + passcode = clientConfiguration.password, + login = clientConfiguration.login, + okHttpRequest = Request.Builder().url( + "http://${clientConfiguration.host}:${clientConfiguration.port}" + ).build() + ) + } + ) + val configuration = Scarlet.Configuration( + lifecycle = clientLifecycleRegistry, + messageAdapterFactories = clientConfiguration.messageAdapterFactories, + streamAdapterFactories = clientConfiguration.streamAdapterFactories, + debug = true + ) + val mainScarlet = Scarlet(protocol, configuration) + return mainScarlet + .child( + OkHttpStompDestination(clientConfiguration.destination), + configuration + ) + .create(clazz) + } + + private fun createOkHttpClient(): OkHttpClient { + return OkHttpClient.Builder() + .writeTimeout(500, TimeUnit.MILLISECONDS) + .readTimeout(500, TimeUnit.MILLISECONDS) + .build() + } + } + + data class Configuration( + val host: String, + val port: Int, + val login: String, + val password: String, + val destination: String, + val messageAdapterFactories: List = emptyList(), + val streamAdapterFactories: List = emptyList() + ) + + companion object { + private val LOGGER = Logger.getLogger(OkHttpStompWebSocketConnection::class.java.name) + + inline fun create( + noinline observeProtocolEvent: SERVICE.() -> Stream, + clientConfiguration: Configuration + ): OkHttpStompWebSocketConnection { + return OkHttpStompWebSocketConnection( + SERVICE::class.java, + observeProtocolEvent, + clientConfiguration + ) + } + } +} \ No newline at end of file From f6469be16710d9dade33d8d9dfb51a7a1eabd439 Mon Sep 17 00:00:00 2001 From: Nikita Zaltsman Date: Thu, 26 Mar 2020 12:06:52 +0700 Subject: [PATCH 31/32] Fix bug on okHttpStompIntegrationTest Change queue to use topic Add overloading. method awaitCountAndCheck with sleep strategy Update StompIntegrationTest Add message to require Fix code style Rewrite stomp message decoder * Rewrite stomp message decoder using okio buffer instead of java buffer * Update okio version Add method awaitCountAndCheck with wait strategy Change using system time millis to use nanoTime * change using system time millis to use nanoTime * add test for websocket connection * fix code style Remove extra method Remove extra comment --- dependencies.gradle | 2 +- scarlet-protocol-stomp/build.gradle | 1 + .../client/OkHttpStompMessageChannel.kt | 2 +- .../okhttp/client/WebSocketConnection.kt | 28 ++--- .../stomp/okhttp/models/StompMessage.kt | 2 - .../okhttp/support/StompMessageDecoder.kt | 106 +++++++++--------- .../scarlet/stomp/StompIntegrationTest.kt | 16 ++- .../okhttp/OkHttpStompIntegrationTest.kt | 21 ++-- .../okhttp/client/WebSocketConnectionTest.kt | 86 ++++++++++++++ .../scarlet/testutils/TestStreamObserver.kt | 11 +- 10 files changed, 183 insertions(+), 92 deletions(-) create mode 100644 scarlet-protocol-stomp/src/test/java/com/tinder/scarlet/stomp/okhttp/client/WebSocketConnectionTest.kt diff --git a/dependencies.gradle b/dependencies.gradle index 171a7a9a..9bc6d880 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -55,7 +55,7 @@ ext { okHttpServerSentEvent = "com.squareup.okhttp3:okhttp-sse:$okHttpVersion" okHttpLoggingInterceptor = "com.squareup.okhttp3:logging-interceptor:$okHttpVersion" - okio = 'com.squareup.okio:okio:1.13.0' + okio = 'com.squareup.okio:okio:2.5.0' mockWebServer = 'com.squareup.okhttp3:mockwebserver:3.11.0' timber = 'com.jakewharton.timber:timber:4.6.0' okSse = 'com.github.heremaps:oksse:0.9.0' diff --git a/scarlet-protocol-stomp/build.gradle b/scarlet-protocol-stomp/build.gradle index fa130972..3962324b 100644 --- a/scarlet-protocol-stomp/build.gradle +++ b/scarlet-protocol-stomp/build.gradle @@ -12,6 +12,7 @@ dependencies { implementation project(':scarlet-core-internal') implementation rootProject.ext.rxJava + implementation rootProject.ext.okio implementation rootProject.ext.kotlinStdlib api rootProject.ext.okHttp diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/client/OkHttpStompMessageChannel.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/client/OkHttpStompMessageChannel.kt index 5062abc2..3cef1e63 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/client/OkHttpStompMessageChannel.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/client/OkHttpStompMessageChannel.kt @@ -46,7 +46,7 @@ class OkHttpStompMessageChannel( } override fun createMessageQueue(listener: MessageQueue.Listener): MessageQueue { - require(messageQueueListener == null) + require(messageQueueListener == null) { "message queue was already created" } messageQueueListener = listener return this } diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/client/WebSocketConnection.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/client/WebSocketConnection.kt index af554214..17da3a6c 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/client/WebSocketConnection.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/client/WebSocketConnection.kt @@ -10,13 +10,15 @@ import com.tinder.scarlet.stomp.okhttp.support.StompMessageDecoder import com.tinder.scarlet.stomp.okhttp.support.StompMessageEncoder import okhttp3.WebSocket import java.util.concurrent.Executors +import java.util.concurrent.ScheduledExecutorService import java.util.concurrent.TimeUnit /** * Okhttp websocket based implementation of {@link Connection}. */ class WebSocketConnection( - private val webSocket: WebSocket + private val webSocket: WebSocket, + private val executor: ScheduledExecutorService = Executors.newSingleThreadScheduledExecutor() ) : Connection, MessageHandler { @Volatile @@ -25,8 +27,6 @@ class WebSocketConnection( @Volatile private var lastWriteTime: Long = -1 - private val executor = Executors.newSingleThreadScheduledExecutor() - private val messageEncoder = StompMessageEncoder() private val messageDecoder = StompMessageDecoder() @@ -42,34 +42,36 @@ class WebSocketConnection( override fun sendMessage(message: StompMessage): Boolean { val lastWriteTime = lastWriteTime if (lastWriteTime != -1L) { - this.lastWriteTime = System.currentTimeMillis() + this.lastWriteTime = System.nanoTime() } - val encodedMessage = messageEncoder.encode(message) - return webSocket.send(String(encodedMessage)) + val encodedMessage = messageEncoder.encode(message).toString(Charsets.UTF_8) + return webSocket.send(encodedMessage) } /** * {@inheritDoc} */ override fun onReceiveInactivity(duration: Long, runnable: () -> Unit) { - lastReadTime = System.currentTimeMillis() + check(duration > 0) { "Duration must be more than 0" } + lastReadTime = System.nanoTime() executor.scheduleWithFixedDelay({ - if (System.currentTimeMillis() - lastReadTime > duration) { + if ((System.nanoTime() - lastReadTime) > TimeUnit.MILLISECONDS.toNanos(duration)) { runnable.invoke() } - }, 0, duration / 2, TimeUnit.MILLISECONDS) + }, 0, duration / 2L, TimeUnit.MILLISECONDS) } /** * {@inheritDoc} */ override fun onWriteInactivity(duration: Long, runnable: () -> Unit) { - lastWriteTime = System.currentTimeMillis() + check(duration > 0) { "Duration must be more than 0" } + lastWriteTime = System.nanoTime() executor.scheduleWithFixedDelay({ - if (System.currentTimeMillis() - lastWriteTime > duration) { + if ((System.nanoTime() - lastWriteTime) > TimeUnit.MILLISECONDS.toNanos(duration)) { runnable.invoke() } - }, 0, duration / 2, TimeUnit.MILLISECONDS) + }, 0, duration / 2L, TimeUnit.MILLISECONDS) } /** @@ -94,7 +96,7 @@ class WebSocketConnection( override fun handle(data: ByteArray): StompMessage? { val lastReadTime = lastReadTime if (lastReadTime != -1L) { - this.lastReadTime = System.currentTimeMillis() + this.lastReadTime = System.nanoTime() } return messageDecoder.decode(data) } diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/models/StompMessage.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/models/StompMessage.kt index daa52a6a..3bfa4369 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/models/StompMessage.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/models/StompMessage.kt @@ -74,6 +74,4 @@ class StompMessage private constructor( "headers=$headers" + ")" } - - } \ No newline at end of file diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/support/StompMessageDecoder.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/support/StompMessageDecoder.kt index 119827ca..83ea39c5 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/support/StompMessageDecoder.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/support/StompMessageDecoder.kt @@ -5,8 +5,10 @@ package com.tinder.scarlet.stomp.okhttp.support import com.tinder.scarlet.stomp.okhttp.models.StompCommand import com.tinder.scarlet.stomp.okhttp.models.StompMessage +import okio.BufferedSource +import okio.buffer +import okio.source import java.io.ByteArrayOutputStream -import java.nio.ByteBuffer /** * An decoder for STOMP frames. @@ -18,18 +20,18 @@ class StompMessageDecoder { * @param array the array to decode */ fun decode(array: ByteArray): StompMessage? { - val byteBuffer = ByteBuffer.wrap(array) - return decode(byteBuffer) + val buffer = array.inputStream().source().buffer() + return decode(buffer) } - private fun decode(byteBuffer: ByteBuffer): StompMessage? { + fun decode(byteBuffer: BufferedSource): StompMessage? { skipLeadingEol(byteBuffer) val stompCommand = readCommand(byteBuffer) ?: return null return if (stompCommand != StompCommand.HEARTBEAT) { val headerAccessor = StompHeaderAccessor.of() - val payload = if (byteBuffer.isNotEmpty()) { + val payload = if (!byteBuffer.exhausted()) { readHeaders(byteBuffer, headerAccessor) readPayloadOrNull(byteBuffer, headerAccessor) ?: return null } else { @@ -46,44 +48,46 @@ class StompMessageDecoder { } } - private fun skipLeadingEol(byteBuffer: ByteBuffer) { + private fun skipLeadingEol(byteBuffer: BufferedSource) { while (true) { if (!tryConsumeEndOfLine(byteBuffer)) break } } private fun readPayloadOrNull( - byteBuffer: ByteBuffer, + bufferedSource: BufferedSource, headerAccessor: StompHeaderAccessor ): ByteArray? { val contentLength = headerAccessor.contentLength return if (contentLength != null && contentLength >= 0) { - readPayloadWithContentLength(byteBuffer, contentLength) + readPayloadWithContentLength(bufferedSource, contentLength) } else { - readPayloadWithoutContentLength(byteBuffer) + readPayloadWithoutContentLength(bufferedSource) } } private fun readPayloadWithContentLength( - byteBuffer: ByteBuffer, + bufferedSource: BufferedSource, contentLength: Int - ) = byteBuffer - .takeIf { buffer -> buffer.remaining() > contentLength } - ?.let { buffer -> - val payload = ByteArray(contentLength) - buffer.get(payload) - - val lastSymbolIsNullOctet = byteBuffer.get().toInt() == 0 - check(lastSymbolIsNullOctet) { "Frame must be terminated with a null octet" } - payload - } + ): ByteArray? { + if (bufferedSource.exhausted()) return null + + val payload = ByteArray(contentLength) + bufferedSource.read(payload) + + if (bufferedSource.exhausted()) return null + val lastSymbolIsNullOctet = bufferedSource.readUtf8CodePoint() == 0 + check(lastSymbolIsNullOctet) { "Frame must be terminated with a null octet" } - private fun readPayloadWithoutContentLength(byteBuffer: ByteBuffer): ByteArray? { + return payload + } + + private fun readPayloadWithoutContentLength(buffer: BufferedSource): ByteArray? { val payload = ByteArrayOutputStream(256) - while (byteBuffer.isNotEmpty()) { - val byte = byteBuffer.get() - if (byte.toInt() != 0) { - payload.write(byte.toInt()) + while (!buffer.exhausted()) { + val codePoint = buffer.readUtf8CodePoint() + if (codePoint != 0) { + payload.write(codePoint) } else { return payload.toByteArray() } @@ -91,17 +95,17 @@ class StompMessageDecoder { return null } - private fun readHeaders(byteBuffer: ByteBuffer, headerAccessor: StompHeaderAccessor) { + private fun readHeaders(byteBuffer: BufferedSource, headerAccessor: StompHeaderAccessor) { while (true) { val headerStream = ByteArrayOutputStream(256) var headerComplete = false - while (byteBuffer.hasRemaining()) { + while (!byteBuffer.exhausted()) { if (tryConsumeEndOfLine(byteBuffer)) { headerComplete = true break } - headerStream.write(byteBuffer.get().toInt()) + headerStream.write(byteBuffer.readUtf8CodePoint()) } if (headerStream.size() > 0 && headerComplete) { @@ -114,7 +118,7 @@ class StompMessageDecoder { headerAccessor[headerName] = headerValue } else { - check(byteBuffer.isEmpty()) { "Illegal header: '$header'. A header must be of the form :[]." } + check(byteBuffer.exhausted()) { "Illegal header: '$header'. A header must be of the form :[]." } } } else { break @@ -122,10 +126,10 @@ class StompMessageDecoder { } } - private fun readCommand(byteBuffer: ByteBuffer): StompCommand? { + private fun readCommand(byteBuffer: BufferedSource): StompCommand? { val command = ByteArrayOutputStream(256) - while (byteBuffer.isNotEmpty() && !tryConsumeEndOfLine(byteBuffer)) { - command.write(byteBuffer.get().toInt()) + while (!byteBuffer.exhausted() && !tryConsumeEndOfLine(byteBuffer)) { + command.write(byteBuffer.readUtf8CodePoint()) } val commandString = command.toByteArray().toString(Charsets.UTF_8) return try { @@ -143,18 +147,24 @@ class StompMessageDecoder { * Try to read an EOL incrementing the buffer position if successful. * @return whether an EOL was consumed */ - private fun tryConsumeEndOfLine(byteBuffer: ByteBuffer): Boolean = byteBuffer - .takeIf { buffer -> buffer.isNotEmpty() } - ?.let { buffer -> - when (byteBuffer.get()) { - '\n'.toByte() -> true - '\r'.toByte() -> checkSequence(byteBuffer) - else -> { - buffer.position(buffer.position() - 1) - false - } + private fun tryConsumeEndOfLine(bufferedSource: BufferedSource): Boolean { + if (bufferedSource.exhausted()) return false + val peekSource = bufferedSource.peek() + + return when (peekSource.readUtf8CodePoint().toChar()) { + '\n' -> { + bufferedSource.skip(1) + true } - } ?: false + '\r' -> { + val nextChartIsNewLine = peekSource.readUtf8CodePoint().toChar() == '\n' + check(!peekSource.exhausted() && nextChartIsNewLine) { "'\\r' must be followed by '\\n'" } + bufferedSource.skip(2) + true + } + else -> false + } + } /** * See STOMP Spec 1.2: @@ -181,14 +191,4 @@ class StompMessageDecoder { stringBuilder.append(inString.substring(pos)) return stringBuilder.toString() } - - private fun checkSequence(byteBuffer: ByteBuffer): Boolean { - val nextChartIsNewLine = byteBuffer.get() == '\n'.toByte() - check(byteBuffer.remaining() > 0 && nextChartIsNewLine) { "'\\r' must be followed by '\\n'" } - return true - } - - private fun ByteBuffer.isNotEmpty(): Boolean = remaining() > 0 - - private fun ByteBuffer.isEmpty(): Boolean = remaining() == 0 } diff --git a/scarlet-protocol-stomp/src/test/java/com/tinder/scarlet/stomp/StompIntegrationTest.kt b/scarlet-protocol-stomp/src/test/java/com/tinder/scarlet/stomp/StompIntegrationTest.kt index d03c8418..a7488cb5 100644 --- a/scarlet-protocol-stomp/src/test/java/com/tinder/scarlet/stomp/StompIntegrationTest.kt +++ b/scarlet-protocol-stomp/src/test/java/com/tinder/scarlet/stomp/StompIntegrationTest.kt @@ -10,6 +10,7 @@ import com.tinder.scarlet.testutils.rule.GozirraStompConnection import com.tinder.scarlet.testutils.test import com.tinder.scarlet.ws.Receive import com.tinder.scarlet.ws.Send +import org.apache.activemq.command.ActiveMQDestination import org.apache.activemq.junit.EmbeddedActiveMQBroker import org.apache.activemq.transport.stomp.StompConnection import org.junit.Rule @@ -20,6 +21,8 @@ class StompIntegrationTest { @get:Rule val broker = object : EmbeddedActiveMQBroker() { override fun configure() { + val destination = ActiveMQDestination.createDestination(SERVER_DESTINATION, 0) + brokerService.destinations = arrayOf(destination) brokerService.addConnector(BROKER_URL) } } @@ -31,7 +34,7 @@ class StompIntegrationTest { PORT, LOGIN, PASSWORD, - DESTINATION + CLIENT_DESTINATION ) ) @get:Rule @@ -42,7 +45,7 @@ class StompIntegrationTest { PORT, LOGIN, PASSWORD, - DESTINATION + CLIENT_DESTINATION ) ) @@ -58,7 +61,7 @@ class StompIntegrationTest { connection2.open() LOGGER.info("${queueTextObserver.values}") - queueTextObserver.awaitCountAtLeast(1) // because broker has a bug and it loses messages sometimes + queueTextObserver.awaitCountAndCheck(2) } @Test @@ -73,8 +76,8 @@ class StompIntegrationTest { PASSWORD ) connection1.begin("tx1") - connection1.send(DESTINATION, "message1", "tx1", null) - connection1.send(DESTINATION, "message2", "tx1", null) + connection1.send(CLIENT_DESTINATION, "message1", "tx1", null) + connection1.send(CLIENT_DESTINATION, "message2", "tx1", null) connection1.commit("tx1") connection1.disconnect() @@ -95,7 +98,8 @@ class StompIntegrationTest { private const val LOGIN = "system" private const val PASSWORD = "manager" private const val BROKER_URL = "stomp://$HOST:$PORT" - private const val DESTINATION = "/queue/test" + private const val SERVER_DESTINATION = "queue://test" + private const val CLIENT_DESTINATION = "/queue/test" interface StompQueueTestService { @Receive diff --git a/scarlet-protocol-stomp/src/test/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompIntegrationTest.kt b/scarlet-protocol-stomp/src/test/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompIntegrationTest.kt index 7b07c309..e33dd4fb 100644 --- a/scarlet-protocol-stomp/src/test/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompIntegrationTest.kt +++ b/scarlet-protocol-stomp/src/test/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompIntegrationTest.kt @@ -6,6 +6,8 @@ import com.tinder.scarlet.testutils.rule.OkHttpStompWebSocketConnection import com.tinder.scarlet.testutils.test import com.tinder.scarlet.ws.Receive import com.tinder.scarlet.ws.Send +import io.reactivex.observers.BaseTestConsumer.TestWaitStrategy.SLEEP_100MS +import org.apache.activemq.command.ActiveMQDestination import org.apache.activemq.junit.EmbeddedActiveMQBroker import org.junit.Rule import org.junit.Test @@ -16,6 +18,8 @@ class OkHttpStompIntegrationTest { @get:Rule val broker = object : EmbeddedActiveMQBroker() { override fun configure() { + val destination = ActiveMQDestination.createDestination(SERVER_DESTINATION, 0) + brokerService.destinations = arrayOf(destination) brokerService.addConnector(BROKER_URL) } } @@ -28,7 +32,7 @@ class OkHttpStompIntegrationTest { port = PORT, password = PASSWORD, host = HOST, - destination = DESTINATION + destination = CLIENT_DESTINATION ) ) @@ -40,7 +44,7 @@ class OkHttpStompIntegrationTest { port = PORT, password = PASSWORD, host = HOST, - destination = DESTINATION + destination = CLIENT_DESTINATION ) ) @@ -51,13 +55,13 @@ class OkHttpStompIntegrationTest { firstConnection.open() secondConnection.open() - firstConnection.client.sendText("message1") - firstConnection.client.sendText("message2") + for (index in 0 until 9) { + firstConnection.client.sendText("message $index") + } firstConnection.clientClosure() LOGGER.info("${queueTextObserver.values}") - queueTextObserver.awaitCountAtLeast(1)// because broker has a bug and it loses messages sometimes - + queueTextObserver.awaitCountAndCheck(9, SLEEP_100MS) secondConnection.clientClosure() } @@ -65,11 +69,12 @@ class OkHttpStompIntegrationTest { private val LOGGER = Logger.getLogger(OkHttpStompIntegrationTest::class.java.name) private const val HOST = "localhost" - private const val PORT = 34343 + private const val PORT = 61613 private const val LOGIN = "system" private const val PASSWORD = "manager" private const val BROKER_URL = "ws://$HOST:$PORT" - private const val DESTINATION = "/queue/test" + private const val SERVER_DESTINATION = "topic://queue_test" + private const val CLIENT_DESTINATION = "/topic/test" interface StompOkHttpQueueTestService { @Receive diff --git a/scarlet-protocol-stomp/src/test/java/com/tinder/scarlet/stomp/okhttp/client/WebSocketConnectionTest.kt b/scarlet-protocol-stomp/src/test/java/com/tinder/scarlet/stomp/okhttp/client/WebSocketConnectionTest.kt new file mode 100644 index 00000000..87e6bc34 --- /dev/null +++ b/scarlet-protocol-stomp/src/test/java/com/tinder/scarlet/stomp/okhttp/client/WebSocketConnectionTest.kt @@ -0,0 +1,86 @@ +package com.tinder.scarlet.stomp.okhttp.client + +import com.nhaarman.mockito_kotlin.mock +import com.tinder.scarlet.stomp.okhttp.core.Connection +import okhttp3.WebSocket +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import java.util.concurrent.Executors +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.TimeUnit + +class WebSocketConnectionTest { + + companion object { + private const val TEST_DURATION = 100L + private const val TEST_LONG_DURATION = 10_000L + } + + private lateinit var connection: Connection + private lateinit var scheduledExecutorService: ScheduledExecutorService + private lateinit var webSocket: WebSocket + + @Before + fun setUp() { + webSocket = mock() + scheduledExecutorService = Executors.newSingleThreadScheduledExecutor() + + connection = WebSocketConnection(webSocket, scheduledExecutorService) + } + + @Test(expected = IllegalStateException::class) + fun `cannot set receive callback with 0 duration`() { + connection.onReceiveInactivity(0) {} + } + + @Test(expected = IllegalStateException::class) + fun `cannot set write callback with 0 duration`() { + connection.onWriteInactivity(0) {} + } + + @Test + fun `check that receive callback will be invoked on time`() { + var wasInvoked = false + connection.onReceiveInactivity(TEST_DURATION) { + wasInvoked = true + } + + scheduledExecutorService.awaitTermination(TEST_DURATION * 2, TimeUnit.MILLISECONDS) + assertTrue(wasInvoked) + } + + @Test + fun `check that receive callback won't be invoked if time did't come`() { + var wasInvoked = false + connection.onReceiveInactivity(TEST_LONG_DURATION) { + wasInvoked = true + } + + scheduledExecutorService.awaitTermination(TEST_DURATION * 2, TimeUnit.MILLISECONDS) + assertFalse(wasInvoked) + } + + @Test + fun `check that write callback will be invoked on time`() { + var wasInvoked = false + connection.onWriteInactivity(TEST_DURATION) { + wasInvoked = true + } + + scheduledExecutorService.awaitTermination(TEST_DURATION * 2, TimeUnit.MILLISECONDS) + assertTrue(wasInvoked) + } + + @Test + fun `check that write callback won't be invoked if time did't come`() { + var wasInvoked = false + connection.onWriteInactivity(TEST_LONG_DURATION) { + wasInvoked = true + } + + scheduledExecutorService.awaitTermination(TEST_DURATION * 2, TimeUnit.MILLISECONDS) + assertFalse(wasInvoked) + } +} \ No newline at end of file diff --git a/scarlet-test-utils/src/main/java/com/tinder/scarlet/testutils/TestStreamObserver.kt b/scarlet-test-utils/src/main/java/com/tinder/scarlet/testutils/TestStreamObserver.kt index 4133e23c..05efa8c4 100644 --- a/scarlet-test-utils/src/main/java/com/tinder/scarlet/testutils/TestStreamObserver.kt +++ b/scarlet-test-utils/src/main/java/com/tinder/scarlet/testutils/TestStreamObserver.kt @@ -5,6 +5,7 @@ package com.tinder.scarlet.testutils import com.tinder.scarlet.Stream +import io.reactivex.observers.BaseTestConsumer.TestWaitStrategy import io.reactivex.processors.PublishProcessor import org.assertj.core.api.Assertions.assertThat @@ -25,18 +26,12 @@ class TestStreamObserver(stream: Stream) { val completions: Long get() = testSubscriber.completions() - fun awaitCountAndCheck(exactly: Int) { + fun awaitCountAndCheck(exactly: Int, waitStrategy: Runnable = TestWaitStrategy.SLEEP_10MS) { testSubscriber.assertNoErrors() - testSubscriber.awaitCount(exactly) + testSubscriber.awaitCount(exactly, waitStrategy) assertThat(values).describedAs("values: $values").hasSize(exactly) } - fun awaitCountAtLeast(atLeast: Int) { - testSubscriber.assertNoErrors() - testSubscriber.awaitCount(atLeast) - assertThat(values).isNotEmpty - } - fun awaitValues(vararg valueAsserts: ValueAssert) { awaitCountAndCheck(valueAsserts.size) testSubscriber.assertNoErrors() From bc561c9c27dcaafe6c8e8885a99be385d849a29f Mon Sep 17 00:00:00 2001 From: Nikita Zaltsman Date: Tue, 11 Aug 2020 23:23:25 +0700 Subject: [PATCH 32/32] Add handler for case for unexpected server disconnect --- .../okhttp/client/OkHttpStompMainChannel.kt | 7 +- .../stomp/okhttp/client/WebSocketCode.kt | 18 +++++ .../okhttp/client/WebSocketConnection.kt | 8 +-- .../okhttp/OkHttpStompIntegrationTest.kt | 69 ++++++++++++++++--- .../rule/OkHttpStompWebSocketConnection.kt | 12 ++-- 5 files changed, 90 insertions(+), 24 deletions(-) create mode 100644 scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/client/WebSocketCode.kt diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/client/OkHttpStompMainChannel.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/client/OkHttpStompMainChannel.kt index 1b6cf891..db185531 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/client/OkHttpStompMainChannel.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/client/OkHttpStompMainChannel.kt @@ -214,7 +214,12 @@ class OkHttpStompMainChannel( } override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { - listener.onClosing(this@OkHttpStompMainChannel) + if (WebSocketCode.isUnexpectedClose(code)) { + listener.onFailed(this@OkHttpStompMainChannel, true, null) + this@OkHttpStompMainChannel.connection = null + } else { + listener.onClosing(this@OkHttpStompMainChannel) + } } override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/client/WebSocketCode.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/client/WebSocketCode.kt new file mode 100644 index 00000000..3783cc6f --- /dev/null +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/client/WebSocketCode.kt @@ -0,0 +1,18 @@ +package com.tinder.scarlet.stomp.okhttp.client + +enum class WebSocketCode(val code: Int, val reason: String? = null) { + + CLOSE_NORMAL(1000, "Normal closure"), + CLOSE_GOING_AWAY(1001, "Unexpected closure from the Server"), + CLOSED_NO_STATUS(1005, "Expected close status, received none"), ; + + companion object { + + /** + * @return true is this unexpected situation error code + */ + fun isUnexpectedClose(code: Int): Boolean { + return code == CLOSE_GOING_AWAY.code || code == CLOSED_NO_STATUS.code + } + } +} \ No newline at end of file diff --git a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/client/WebSocketConnection.kt b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/client/WebSocketConnection.kt index 17da3a6c..49468a60 100644 --- a/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/client/WebSocketConnection.kt +++ b/scarlet-protocol-stomp/src/main/java/com/tinder/scarlet/stomp/okhttp/client/WebSocketConnection.kt @@ -30,12 +30,6 @@ class WebSocketConnection( private val messageEncoder = StompMessageEncoder() private val messageDecoder = StompMessageDecoder() - companion object { - - private const val NORMAL_CLOSURE_STATUS_CODE = 1000 - private const val NORMAL_CLOSURE_REASON = "Normal closure" - } - /** * {@inheritDoc} */ @@ -86,7 +80,7 @@ class WebSocketConnection( * {@inheritDoc} */ override fun close() { - webSocket.close(NORMAL_CLOSURE_STATUS_CODE, NORMAL_CLOSURE_REASON) + webSocket.close(WebSocketCode.CLOSE_NORMAL.code, WebSocketCode.CLOSE_NORMAL.reason) executor.shutdown() } diff --git a/scarlet-protocol-stomp/src/test/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompIntegrationTest.kt b/scarlet-protocol-stomp/src/test/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompIntegrationTest.kt index e33dd4fb..3bc1b959 100644 --- a/scarlet-protocol-stomp/src/test/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompIntegrationTest.kt +++ b/scarlet-protocol-stomp/src/test/java/com/tinder/scarlet/stomp/okhttp/OkHttpStompIntegrationTest.kt @@ -9,20 +9,15 @@ import com.tinder.scarlet.ws.Send import io.reactivex.observers.BaseTestConsumer.TestWaitStrategy.SLEEP_100MS import org.apache.activemq.command.ActiveMQDestination import org.apache.activemq.junit.EmbeddedActiveMQBroker +import org.junit.After +import org.junit.Before import org.junit.Rule import org.junit.Test import java.util.logging.Logger class OkHttpStompIntegrationTest { - @get:Rule - val broker = object : EmbeddedActiveMQBroker() { - override fun configure() { - val destination = ActiveMQDestination.createDestination(SERVER_DESTINATION, 0) - brokerService.destinations = arrayOf(destination) - brokerService.addConnector(BROKER_URL) - } - } + private var broker: TestEmbeddedActiveMQBroker = TestEmbeddedActiveMQBroker() @get:Rule val firstConnection = OkHttpStompWebSocketConnection.create( @@ -48,6 +43,16 @@ class OkHttpStompIntegrationTest { ) ) + @Before + fun setUp() { + startServer() + } + + @After + fun tearDown() { + stopServer() + } + @Test fun `correct receive and send messages`() { val queueTextObserver = secondConnection.client.observeText().test() @@ -65,6 +70,54 @@ class OkHttpStompIntegrationTest { secondConnection.clientClosure() } + @Test + fun `reconnect test`() { + val queueTextObserver = secondConnection.client.observeText().test() + + firstConnection.open() + secondConnection.open() + + restartServer() + + firstConnection.clientProtocolEventObserver.awaitCountAndCheck(3) // Open -> Close -> Open + secondConnection.clientProtocolEventObserver.awaitCountAndCheck(3) // Open -> Close -> Open + + for (index in 0 until 9) { + firstConnection.client.sendText("message $index") + } + + firstConnection.clientClosure() + + LOGGER.info("${queueTextObserver.values}") + queueTextObserver.awaitCountAndCheck(9, SLEEP_100MS) + secondConnection.clientClosure() + } + + private fun startServer() { + broker = TestEmbeddedActiveMQBroker() + broker.start() + } + + private fun stopServer() { + broker.stop() + } + + private fun restartServer() { + broker.stop() + broker = TestEmbeddedActiveMQBroker() + broker.start() + } + + private class TestEmbeddedActiveMQBroker : EmbeddedActiveMQBroker() { + + override fun configure() { + val destination = ActiveMQDestination.createDestination(SERVER_DESTINATION, 0) + brokerService.destinations = arrayOf(destination) + brokerService.addConnector(BROKER_URL) + brokerService.isPersistent = false + } + } + companion object { private val LOGGER = Logger.getLogger(OkHttpStompIntegrationTest::class.java.name) diff --git a/scarlet-test-utils/src/main/java/com/tinder/scarlet/testutils/rule/OkHttpStompWebSocketConnection.kt b/scarlet-test-utils/src/main/java/com/tinder/scarlet/testutils/rule/OkHttpStompWebSocketConnection.kt index 84bc6cb9..24465c81 100644 --- a/scarlet-test-utils/src/main/java/com/tinder/scarlet/testutils/rule/OkHttpStompWebSocketConnection.kt +++ b/scarlet-test-utils/src/main/java/com/tinder/scarlet/testutils/rule/OkHttpStompWebSocketConnection.kt @@ -72,14 +72,6 @@ class OkHttpStompWebSocketConnection( lateinit var clientProtocolEventObserver: TestStreamObserver override fun before() { - createClientAndServer() - } - - override fun after() { - clientLifecycleRegistry.onNext(LifecycleState.Completed) - } - - private fun createClientAndServer() { client = createClient() clientProtocolEventObserver = client.observeProtocolEvent().test() client.observeProtocolEvent().start(object : Stream.Observer { @@ -101,6 +93,10 @@ class OkHttpStompWebSocketConnection( }) } + override fun after() { + clientLifecycleRegistry.onNext(LifecycleState.Completed) + } + private fun createClient(): SERVICE { val protocol = OkHttpStompClient( configuration = OkHttpStompMainChannel.Configuration(